Initial implementation.

This commit is contained in:
2023-03-22 12:33:43 -05:00
parent 0dbac771a8
commit 05d688b1b9
6 changed files with 497 additions and 7 deletions

235
src/pit2trellopkg/api.nim Normal file
View File

@ -0,0 +1,235 @@
import std/[httpclient, json, logging, options, strutils, tables, times, uri]
import timeutils, zero_functional
from sequtils import toSeq
type
TrelloListName* = enum
tlnTodo = "Todo"
tlnInProgress = "In Progress"
tlnDone = "Done"
tlnUnknown = "UNKNOWN"
TrelloApiCredentials* = object
key*: string
secret*: string
TrelloLabel* = object
id*, color*, name*: string
labelIds*: seq[string]
TrelloCard* = object
id*, desc*, name*: string
labelIds*: seq[string]
lastUpdatedAt*: DateTime
TrelloList* = object
id*, name*: string
cards*: seq[TrelloCard]
TrelloBoard* = ref object
id*, name*: string
lists*: TableRef[TrelloListName, TrelloList]
labels*: seq[TrelloLabel]
TrelloApiClient* = ref object
http: HttpClient
credentials: TrelloApiCredentials
baseUrl*: string
func getOrFail(node: JsonNode, key: string): JsonNode =
if not node.hasKey(key):
raise newException(ValueError, "missing key '" & key & "'")
return node[key]
func indentLines(s: string, indent = " "): string =
(s.splitLines --> map(indent & it)).join("\p")
func mergeCards(l: TrelloList, c: seq[TrelloCard]): TrelloList =
return TrelloList(
id: l.id,
name: l.name,
cards: l.cards & c)
proc `&`(
params: openArray[(string, string)],
creds: TrelloApiCredentials
): seq[(string, string)] =
result = params.toSeq & {
"key": creds.key,
"token": creds.secret
}.toSeq
proc qp(self: openarray[(string, string)]): string =
let s = self.toSeq
(s --> map(it[0] & "=" & it[1])).join("&")
proc qp(credentials: TrelloApiCredentials): string =
return qp([] & credentials)
func `$`*(c: TrelloCard): string =
return c.name & " (" & c.id & ")"
func `$`*(l: TrelloList): string =
result = l.name & " (" & l.id & ")"
result &= (l.cards --> map("\p" & $it)).join("").indentLines
func `$`*(b: TrelloBoard): string =
result = b.name & " (" & b.id & ")"
result &= (b.lists.values.toSeq --> map("\p" & $it)).join("").indentLines
proc parseTrelloLabel*(json: JsonNode): TrelloLabel =
TrelloLabel(
id: json.getOrFail("id").getStr,
name: json.getOrFail("name").getStr,
color: json.getOrFail("color").getStr)
proc parseTrelloCard*(json: JsonNode): TrelloCard =
result = TrelloCard(
id: json.getOrFail("id").getStr,
name: json.getOrFail("name").getStr,
desc: json["desc"].getStr(""),
lastUpdatedAt: parseIso8601(json.getOrFail("dateLastActivity").getStr),
labelIds: json["idLabels"].getElems() --> map(it.getStr()))
proc createLabel*(
self: TrelloApiClient,
board: TrelloBoard,
name: string,
color = "blue"
): TrelloLabel =
let params = {
"name": encodeUrl(name),
"color": color
}
let url = self.baseUrl & "/1/boards/" & board.id & "/labels"
debug "Creating a new label: POST " & url & "?" & params.qp
let resp = self.http.post(url & "?" & qp(params & self.credentials))
if not resp.code.is2xx:
raise newException(IOError,
"unable to create new label on board '" & board.id & "': " & resp.body)
return parseTrelloLabel(parseJson(resp.body))
proc loadCards*(self: TrelloApiClient, listId: string): seq[TrelloCard] =
let cardsUrl = self.baseUrl & "/1/lists/" & listId & "/cards"
debug "Loading cards on list: GET " & cardsUrl
let cardsResp = self.http.get(cardsUrl & "?" & qp(self.credentials))
if not cardsResp.code.is2xx:
raise newException(IOError,
"unable to fetch cards for list '" & listId & "': " & cardsResp.body)
let cardsJson = parseJson(cardsResp.body)
result = cardsJson.getElems.toSeq --> map(it.parseTrelloCard)
proc createCard*(
self: TrelloApiClient,
c: TrelloCard,
targetList: TrelloList
): TrelloCard =
let params = {
"name": encodeUrl(c.name),
"desc": encodeUrl(c.desc),
"idLabels": c.labelIds.join(","),
"idList": targetList.id
}
let url = self.baseUrl & "/1/cards"
debug "Creating a new card: POST " & url & "?" & params.qp
let resp = self.http.post(url & "?" & qp(params & self.credentials))
if not resp.code.is2xx:
raise newException(IOError,
"unable to create a card on list '" & targetList.id & "'")
return parseTrelloCard(parseJson(resp.body))
proc updateCard*(
self: TrelloApiClient,
c: TrelloCard,
targetList: TrelloList
) =
let params = {
"name": encodeUrl(c.name),
"desc": encodeUrl(c.desc),
"idList": targetList.id,
"idLabels": c.labelIds.join(",")
}
let url = self.baseUrl & "/1/cards/" & c.id
debug "Updating card: PUT " & url & "?" & params.qp
let resp = self.http.put(url & "?" & qp(params & self.credentials))
if not resp.code.is2xx:
raise newException(IOError,
"unable to update card for id '" & c.id & "': " & resp.body)
proc loadBoard*(self: TrelloApiClient, boardId: string): TrelloBoard =
let boardUrl = self.baseUrl & "/1/boards/" & boardId
debug "Loading board: GET " & boardUrl & "?" & qp(self.credentials)
let boardResp = self.http.get(boardUrl & "?" & qp(self.credentials))
if not boardResp.code.is2xx:
raise newException(IOError,
"unable to fetch board for id '" & boardId & "': " & boardResp.body)
let boardJson = parseJson(boardResp.body)
result = TrelloBoard(
id: boardJson.getOrFail("id").getStr,
name: boardJson.getOrFail("name").getStr,
labels: @[])
let labelsUrl = self.baseUrl & "/1/boards/" & boardId & "/labels"
debug "Loading labels: GET " & boardUrl
let labelsResp = self.http.get(labelsUrl & "?" & qp(self.credentials))
if labelsResp.code.is2xx:
result.labels = parseJson(labelsResp.body).getElems.toSeq -->
map(it.parseTrelloLabel).
filter(not isEmptyOrWhitespace(it.name))
else:
warn "unable to fetch labels for board id '" & boardId & "': " & labelsResp.body
let listsUrl = self.baseUrl & "/1/boards/" & boardId & "/lists"
debug "Loading lists: GET " & boardUrl
let listsResp = self.http.get(listsUrl & "?" & qp(self.credentials))
if not listsResp.code.is2xx:
raise newException(IOError,
"unable to fetch lists for board id '" & boardId & "': " & listsResp.body)
let listsJson = parseJson(listsResp.body)
let lists: seq[TrelloList] =
listsJson.getElems.toSeq -->
map(TrelloList(
id: it.getOrFail("id").getStr,
name: it.getOrFail("name").getStr,
cards: self.loadCards(it.getOrFail("id").getStr)))
result.lists = newTable[TrelloListName, TrelloList]()
result.lists[tlnUnknown] = TrelloList(id: "", name: "Unknown", cards: @[])
for lentList in lists:
let l = lentList
let knownList = TrelloListName.items.toSeq --> find($it == l.name)
if knownList.isSome: result.lists[knownList.get] = l
else:
result.lists[tlnUnknown] = mergeCards(result.lists[tlnUnknown], l.cards)
for listName in TrelloListName:
if not result.lists.contains(listName):
raise newException(ValueError,
"board '" & result.name & "' has no list named '" & $listName & "'")
debug $result
proc initTrelloApiClient*(
cred: TrelloApiCredentials,
baseUrl: string
): TrelloApiClient =
result = TrelloApiClient(
http: newHttpClient(),
credentials: cred,
baseUrl: baseUrl)

View File

@ -0,0 +1,15 @@
const PIT2TRELLO_VERSION* = "0.1.0"
const USAGE* = """
Usage:
pit2trello sync [options]
Options:
--config <cfgFile> Path to the pit configuration file. By default this is
$HOME/.pitrc
--debug Enable verbose debug logging.
--help Print this help and usage information.
"""