Initial implementation.
This commit is contained in:
235
src/pit2trellopkg/api.nim
Normal file
235
src/pit2trellopkg/api.nim
Normal 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)
|
15
src/pit2trellopkg/cliconstants.nim
Normal file
15
src/pit2trellopkg/cliconstants.nim
Normal 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.
|
||||
"""
|
Reference in New Issue
Block a user