Compare commits

...

8 Commits
0.3.1 ... main

5 changed files with 92 additions and 24 deletions

14
README.md Normal file
View File

@ -0,0 +1,14 @@
# Hope Family Fellowship Notion API Client
## Building
### Software Packages
`hff_notion_api_client` depends on a number of packages which are not yet
available in he official Nim repository. The easiest way to get access to these
packages is to add a new `PackageList` to your [nimble configuration] for the
[JDB Software Nim packages repository]. The url is
`https://git.jdb-software.com/jdb/nim-packages/raw/main/packages.json`
[nimble configuration]: https://github.com/nim-lang/nimble#configuration
[JDB Software Nim packages]: https://git.jdb-software.com/jdb/nim-packages

View File

@ -1,14 +1,14 @@
# Package
version = "0.3.1"
version = "0.5.0"
author = "Jonathan Bernard"
description = "Utilities and bindings for HFF's Notion API."
license = "GPL-3.0-or-later"
srcDir = "src"
# Dependencies
requires "nim >= 1.4.8"
requires "https://git.jdb-software.com/jdb/buffoonery"
requires "https://git.jdb-software.com/jdb/nim-time-utils.git >= 0.5.0"
# packages from git.jdb-software.com/jdb/nim-packages
requires @["buffoonery", "timeutils >= 0.5.0", "namespaced_logging >= 0.3.1"]

View File

@ -1,11 +1,8 @@
import std/httpclient, std/json, std/logging, std/sequtils, std/tables, std/strutils
import std/[httpclient, json, logging, options, sequtils, tables, strutils]
import buffoonery/jsonutils
import hff_notion_api_client/config
import hff_notion_api_client/models
import hff_notion_api_client/sequtils_ext
import hff_notion_api_client/utils
import hff_notion_api_client/[config, models, sequtils_ext, utils]
type
NotionClientConfig* = object
@ -61,20 +58,27 @@ proc fetchAllPages*(
bodyTemplate = %*{ "page_size": NOTION_MAX_PAGE_SIZE}): seq[JsonNode] =
result = @[]
var nextCursor: string = ""
var nextCursor: Option[string] = none[string]()
while true:
let body = parseJson($bodyTemplate)
if not nextCursor.isEmptyOrWhitespace: body["start_cursor"] = %nextCursor
if nextCursor.isSome: body["start_cursor"] = %nextCursor.get
debug "Fetching pages from database:\n\tPOST " & url & "\n\t" & $body
let jsonResp = parseJson(http.postContent(url, $body))
result = result & jsonResp["results"].getElems
if jsonResp.hasKey("next_cursor") and jsonResp["next_cursor"].kind != JNull:
nextCursor = jsonResp["next_cursor"].getStr
nextCursor = some(jsonResp["next_cursor"].getStr)
else: break
if nextCursor.isEmptyOrWhitespace: break
template fetchDatabaseObject*(notion: NotionClient, dbId: string): untyped =
let resp = notion.http.get(notion.apiBaseUrl & "/databases/" & dbId)
if not resp.status.startsWith("2"):
debug resp.body
raise newException(HttpRequestError, "API Request failed: " & resp.body)
parseJson(resp.body)
template fetchPage*(notion: NotionClient, pageId: string): untyped =
let resp = notion.http.get(notion.apiBaseUrl & "/pages/" & pageId)

View File

@ -11,6 +11,7 @@ type
city*: string
state*: string
street*: string
suiteOrBuilding*: string
zipCode*: string
createdAt*: Option[DateTime]
lastUpdatedAt*: Option[DateTime]
@ -19,8 +20,11 @@ type
FamilyObj* = object
id*: string
name*: string
headsOfHousehold*: seq[string]
headOfHouseholdIds*: seq[string]
primaryAddressId*: seq[string]
primaryAddress*: string
primaryAddressId*: string
members*: seq[string]
memberIds*: seq[string]
createdAt*: Option[DateTime]
lastUpdatedAt*: Option[DateTime]
@ -35,10 +39,14 @@ type
gender*: string
primaryPhoneNumber*: string
primaryEmailAddress*: string
addresses*: seq[string]
addressIds*: seq[string]
marriedToId*: seq[string]
marriedTo*: string
marriedToId*: string
anniversary*: Option[DateTime]
parents*: seq[string]
parentIds*: seq[string]
children*: seq[string]
childIds*: seq[string]
relationshipToHff*: seq[string]
createdAt*: Option[DateTime]
@ -78,8 +86,8 @@ func sameContents[T](a: seq[T], b: seq[T]): bool =
func `==`*(a, b: Family): bool =
return a.name == b.name and
sameContents(a.headOfHouseholdIds, b.headOfHouseholdIds) and
sameContents(a.primaryAddressId, b.primaryAddressId) and
sameContents(a.memberIds, b.memberIds)
sameContents(a.memberIds, b.memberIds) and
a.primaryAddressId == b.primaryAddressId
func `==`*(a, b: Person): bool =
return a.preferredName == b.preferredName and
@ -91,8 +99,8 @@ func `==`*(a, b: Person): bool =
a.primaryPhoneNumber == b.primaryPhoneNumber and
a.primaryEmailAddress == b.primaryEmailAddress and
a.anniversary == b.anniversary and
a.marriedToId == b.marriedToId and
sameContents(a.addressIds, b.addressIds) and
sameContents(a.marriedToId, b.marriedToId) and
sameContents(a.parentIds, b.parentIds) and
sameContents(a.childIds, b.childIds) and
sameContents(a.relationshipToHff, b.relationshipToHff)
@ -120,6 +128,7 @@ func toPage*(a: Address): JsonNode =
"City": makeTextProp("rich_text", a.city),
"State": makeSelectProp(a.state),
"Street Address": makeTextProp("title", a.street),
"Suite or Building": makeTextProp("rich_text", a.suiteOrBuilding),
"Zip Code": makeTextProp("rich_text", a.zipCode)
}
}
@ -129,7 +138,7 @@ func toPage*(f: Family): JsonNode =
"properties": {
"Name": makeTextProp("title", f.name),
"Head(s) of Household": makeRelationProp(f.headOfHouseholdIds),
"Primary Address": makeRelationProp(f.primaryAddressId),
"Primary Address": makeRelationProp(@[f.primaryAddressId]),
"Members": makeRelationProp(f.memberIds)
}
}
@ -147,7 +156,7 @@ func toPage*(p: Person): JsonNode =
"Email Address": { "email": p.primaryEmailAddress },
"Relationship to HFF": makeMultiSelectProp(p.relationshipToHff),
"Address": makeRelationProp(p.addressIds),
"Married To": makeRelationProp(p.marriedToId),
"Married To": makeRelationProp(@[p.marriedToId]),
"Anniversary": makeDateProp(p.anniversary),
"Parents": makeRelationProp(p.parentIds),
"Children": makeRelationProp(p.childIds),
@ -175,21 +184,30 @@ proc addressFromPage*(page: JsonNode): Address =
city: page.getText("City"),
state: page.getSelect("State"),
street: page.getTitle("Street Address"),
suiteOrBuilding: page.getText("Suite or Building"),
zipCode: page.getText("Zip Code"),
createdAt: some(parseIso8601(page["created_time"].getStr)),
lastUpdatedAt: some(parseIso8601(page["last_edited_time"].getStr)))
proc familyFromPage*(page: JsonNode): Family =
let primaryAddressIds = page.getRelationIds("Primary Address")
result = Family(
id: page["id"].getStr,
name: page.getTitle("Name"),
headsOfHousehold: page.getRolledupRecordTitles("Head(s) of Household (display)"),
headOfHouseholdIds: page.getRelationIds("Head(s) of Household"),
primaryAddressId: page.getRelationIds("Primary Address"),
primaryAddress: if primaryAddressIds.len == 0: ""
else: page.getRolledupRecordTitles("Primary Address (display)")[0],
primaryAddressId: if primaryAddressIds.len == 0: ""
else: primaryAddressIds[0],
members: page.getRolledupRecordTitles("Members (display)"),
memberIds: page.getRelationIds("Members"),
createdAt: some(parseIso8601(page["created_time"].getStr)),
lastUpdatedAt: some(parseIso8601(page["last_edited_time"].getStr)))
proc personFromPage*(page: JsonNode): Person =
let marriedToIds = page.getRelationIds("Married To")
result = Person(
id: page["id"].getStr,
preferredName: page.getTitle("Preferred Name"),
@ -201,10 +219,16 @@ proc personFromPage*(page: JsonNode): Person =
primaryPhoneNumber: page.getPhone("Primary Phone Number"),
primaryEmailAddress: page.getEmail("Email Address"),
relationshipToHff: page.getMultiSelect("Relationship to HFF"),
addresses: page.getRollupArrayValues("Full Address").mapIt(it{"formula", "string"}.getStr),
addressIds: page.getRelationIds("Address"),
marriedToId: page.getRelationIds("Married To"),
marriedTo: if marriedToIds.len == 0: ""
else: page.getRolledupRecordTitles("Married To (display)")[0],
marriedToId: if marriedToIds.len == 0: ""
else: marriedToIds[0],
anniversary: page.getDateTime("Anniversary"),
parents: page.getRolledupRecordTitles("Parents (display)"),
parentIds: page.getRelationIds("Parents"),
children: page.getRolledupRecordTitles("Children (display)"),
childIds: page.getRelationIds("Children"),
createdAt: some(parseIso8601(page["created_time"].getStr)),
lastUpdatedAt: some(parseIso8601(page["last_edited_time"].getStr)),

View File

@ -1,9 +1,16 @@
import std/json, std/options, std/sequtils, std/times
import std/json, std/logging, std/options, std/sequtils, std/times
import namespaced_logging
import timeutils
const NOTION_MAX_PAGE_SIZE* = 100
const NOTION_DATE_FORMAT = "YYYY-MM-dd"
var logNs {.threadvar.}: LoggingNamespace
template log(): untyped =
if logNs.isNil: logNs = getLoggerForNamespace("hff_notion_api_client/utils", lvlInfo)
logNs
proc parseDate(str: string): DateTime =
try: result = parseIso8601(str)
except: result = times.parse(str, NOTION_DATE_FORMAT)
@ -50,7 +57,7 @@ proc makeTextProp*(propType: string, value: string): JsonNode =
## Utility functions for reading Page property values
## --------------------------------------------------
proc getPropNode(page: JsonNode, propType, propName: string): JsonNode =
proc getPropNode*(page: JsonNode, propType, propName: string): JsonNode =
result = page{"properties", propName, propType}
if isNil(result):
@ -74,6 +81,25 @@ proc getRelationIds*(page: JsonNode, propName: string): seq[string] =
let propNode = page.getPropNode("relation", propName)
return propNode.getElems.mapIt(it["id"].getStr)
proc getRollupArrayValues*(page: JsonNode, propName: string): seq[JsonNode] =
return page.getPropNode("rollup", propName){"array"}.getElems(@[])
proc getRolledupRecordTitles*(page: JsonNode, propName: string): seq[string] =
let rollups = page.getPropNode("rollup", propName)["array"]
if rollups.getElems.len == 0: return @[]
result = @[]
for rollupItem in rollups.getElems:
if not rollupItem.hasKey("title") or rollupItem["title"].getElems.len != 1:
log().debug "Expected exactly one item of type 'title'. Received: \n" &
rollupItem.pretty
raise newException(
ValueError,
"unexpected format of rollup value for '" & propName & "' property")
result.add(rollupItem["title"].getElems[0]["plain_text"].getStr)
proc getText*(page: JsonNode, propName: string): string =
let propNode = page.getPropNode("rich_text", propName)
if propNode.len == 0: return ""