Knackpy #
Knackpy is a Python client for interacting with Knack applications.
Here’s the complete documentation.
Installation #
$ pip install knackpy
Quick Start #
>>> import knackpy
# basic app construction
>>> app = knackpy.App(app_id="myappid", api_key="myverysecretapikey")
# fetch all records from 'object_1'
>>> records = app.get("object_1")
# get the formatted keys/values of each record
>>> records_formatted = [record.format() for record in records]
# access a record property by name
>>> customer_address = records[0]["Customer Address"]
# create a record
>>> data = {"field_1": "pizza"}
>>> record = app.record(method="create", data=data, obj="object_1")
# download files from an object or view
>>> app.download(
... container="object_1",
... field="field_1",
... out_dir="_downloads"
... )
# upload a file and attach it to a record
>>> app.upload(
... container="object_1", # must be an object key or name
... field="field_3",
... path="file.jpg",
... asset_type="file", # must be 'file' or 'image', depending on field type
... record_id="5d7968c8092e7f00106c6399"
... )
Working with Apps #
Knackpy is designed around the App
class. It provides helpers for querying
and manipulating Knack application data. You should use the App
class
because:
- It allows you to query objects and views by key or name
- It takes care of localization issues
- It lets you download and upload files from your app.
- It does other things, too.
To create an App
instance, the bare minimum you need to provide is your application ID.
If you construct an App
instance without providing an API key, you will only be able to fetch records from publicly-availble views.
Note that fetching data from public views is a smart way to avoid hitting your API limit.
# basic app construction with api key
>>> import knackpy
>>> app = knackpy.App(app_id="myappid", api_key="myverysecretapikey")
Getting Records #
Use App.get()
to fetch records from a Knack application. Container identifiers can be supplied as a key (object_1
, view_1
) or a name (my_exciting_object
, My Exciting View
).
# fetch all records from object_1
>>> records = app.get("object_1")
# or
# fetch all records from view named "My Exciting View"
>>> records = app.get("My Exciting View")
For faster performance, set generate=true
when getting records from your app. This will prevent knackpy from constructing all Record
instances up-front by returning a generator function instead of a list.
>>> records = app.get("object_1", generate=true)
>>> for record in records:
... formatted_record = record.format()
Be Careful When Using Named References #
Namespace conflicts are highly likely when fetching by name, because Knack uses object names as the default name for views. If you attempt to query your application by a name that exists as both an object and a view, Knackpy will raise a ValueError
.
You can reuse the same App
instance to fetch records from other objects and views.
>>> app.get("my_exciting_object")
>>> app.get("my_boring_object")
>>> app.get("view_1")
Omitting the Container Name #
If you’ve only fetched one container, you can omit the container name when accessing your records. This is helpful during development, but for readability we suggest you avoid this practice in production code.
>>> records = app.get("My Exciting View")
# you can omit the container name if you want to access your records again
>>> same_records_without_accessor = app.get()
You can refine your record requests by specififying a record_limit
, filters
, and timeout
. See the module documentaiton for details.
>>> filters = {
"match": "or",
"rules": [
{"field": "field_1", "operator": "is", "value": 1},
{"field": "field_2", "operator": "is", "value": "Pizza"},
],
}
>>> records = app.get("object_1", record_limit=10, filters=filters)
Creating, Updating, and Deleting Records #
Create a record.
>>> app = knackpy.App(app_id="myappid", api_key="myverysecretapikey")
>>> data = {"field_1": "pizza"}
>>> record = app.record(method="create", data=data, obj="object_1")
Update a record.
>>> app = knackpy.App(app_id="myappid", api_key="myverysecretapikey")
>>> record = app.get("object_1")[0]
>>> data = dict(record)
>>> data["field_1"] = "new value"
>>> record = app.record(method="update", data=data, obj="object_1")
Delete a record.
>>> response = app.record(method="delete", data={"id": "abc123xyz789"}, obj="object_1")
# response == {"delete": True}
Download Files #
Download files from an object or view.
-
container
(str
): The name or key of the object or view from which files will be downloaded. -
field
(str
): The field key of the file or image field to be downloaded. -
out_dir
(str
, optional): Relative path to the directory to which files will be written. Defaults to “_downloads”. -
label_keys
(list
, optional): A list of field keys whose values will be prepended to the attachment filename, separated by an underscore.
>>> app.download(
... container="object_1",
... field="field_1",
... out_dir="_downloads",
... label_keys="field_2"
... )
Upload Files #
Upload a file and attach it to a Knack record.
>>> res = app.upload(
... container="object_1", # must be an object key or name
... field="field_3",
... path="file.jpg",
... asset_type="file", # must be 'file' or 'image', depending on field type
... record_id="5d7968c8092e7f00106c6399"
... )
Advanced App
Usage
#
Raw record data is available at App.data
. You can use this property to check the readily available data in your App instance.
>>> app.data.keys()
["object_1", "object_2", "view_1"]
References to all available endpoints are stored at App.containers
. This is handy if you want to check the name of a container, or its key:
>>> app.containers
[
Container(obj='object_1', view=None, scene=None, name='my_boring_object'),
Container(obj='object_2', view=None, scene=None, name='my_exciting_object'),
Container(obj=None, view='view_1', scene='scene_1', name='My Exciting View'),
]
You can avoid an API call by providing your own Knack metadata when creating an App
instance (unclear if metadata requests count against API limits)
>>> import json
# get your app's metadata here: https://loader.knack.com/v1/applications/<app_id:str>"
>>> with open("my_metadata.json", "r") as fin:
... metadata = json.loads(fin.read())
>> app = knackpy.App(app_id, metadata=metadata)
You can side-load record data into your your app as well. Note that you must assign your data to a valid key that exists in your app.
>>> with open("my_knack_data.json", "r") as fin:
... data = { "object_3": json.loads(fin.read()) }
>> app.data = data
>> records = app.get("object_3")
You can use knackpy.get()
to fetch “raw” data from your Knack app. Be aware that raw Knack timestamps are problematic. See the Records documentation.
Other App
Methods
#
Display summary metrics about the app.
>>> app.info()
{'objects': 10, 'scenes': 4, 'records': 6786, 'size': '25.47mb'}
Write a container to CSV. Be aware that destination files will be overwritten, if they exist.
>>> app.to_csv("my exciting view", out_dir="_csv")
Working with Record
Objects
#
Record
objects are dict
-like containers for Knack record data. Note that all timestamps have been correctly set to unix time.
You can access a record value like you would a dict
, using the field key or field name.
>>> record = app.get("object_1")[0]
# access a value via field key
>>> record["field_22"]
{"city": "Austin", "state": "TX", "street": "8700 Cameron Rd", "street2": "Suite 1", "zip": "78754"}
# access a value via field name
>>> record["Customer Address"]
{"city": "Austin", "state": "TX", "street": "8700 Cameron Rd", "street2": "Suite 1", "zip": "78754"}
Formatting Records #
Format the keys and/or values of a record.
# format keys and values
>>> formatted_record = record.format()
{ "id": "abc123xyz789", "Customer Address": "8700 Cameron Rd, Austin, TX, 78754" }
# only format the keys
>>> formatted_keys = record.format(values=False)
{ "id": "abc123xyz789", "Customer Address": {"city": "Austin", "state": "TX", "street": "8700 Cameron Rd", "street2": "Suite 1", "zip": "78754"}}
# only format the values
>>> formatted_values = record.format(keys=False)
{ "id": "abc123xyz789", "field_22": "8700 Cameron Rd, Austin, TX, 78754" }
# format specific keys and values
>>> formatted_values = record.format(keys=["field_22"], values=["field_22"])
{ "id": "abc123xyz789", "Customer Address": "8700 Cameron Rd, Austin, TX, 78754" }
Although a Record
object looks like a dict
, it contains Field
objects (TODO: link to docs). If you want to convert a Record
object into a plain-old dict
, use the dict()
built-in.
>>> record = app.get("object_1")[0]
>>> data = dict(record)
Dict-like Record
methods (Items, Keys, and Names)
#
Record.items()
: returns a list of the knackpyField
objects contained within theRecord
.Record.keys()
: returns a list (not aview
) of the record’s field keys.Record.names()
: returns a list of the record’s field names.
Accessing Raw Knack Data #
Records may look raw, but any timestamps have been corrected to real unix time. If you want the raw, untouched data, use the
Record.raw
property.
Accessing the API Directly #
If you don’t want to fuss with App
objects, you can use knackpy’s direct interface with the Knack API.
# This is equivalent to exporting records in JSON format from the Knack Builder
>>> import knackpy
>>> data = knackpy.get(
... app_id="myappid",
... api_key="myverysecretapikey",
... obj="object_1",
... record_limit=None,
... timeout=30
... )
See the API module documentation for further detail.
Timestamps and Localization #
Although the Knack API returns timestamp values as Unix timestamps in millesconds, these raw values represent millisecond timestamps in your localized timezone. For example a Knack timestamp value of 1578254700000
represents Sunday, January 5, 2020 8:05:00 PM local time.
To address this, the App
class converts Knack timestamps into real (UTC-based) unix timestamps. Timestamps are corrected by referencing the timezone info in your apps metadata. You can manually override your app’s timezone information by passing an IANA-compliant timezone string to your App
instance, like so:
>>> app = knackpy.App(app_id="myappid", api_key="myverysecretapikey", tzinfo="US/Eastern")
If you’d like to access your raw, uncorrected records, they can be found at App.data[<container_name:str>]
.
Note also that Record
objects return corrected timestamps via Record.raw
and Record.format()
. So,
>>> app = knackpy.App(app_id="myappid", api_key="myverysecretapikey", tzinfo="US/Eastern")
# yields raw records with corrected millisecond timestamps
>>> records_raw = [record.raw for record in app.get("object_3")]
# yields corrected timestamps as ISO-8601 strings
>>> records_formatted = [record.format() for record in app.get("object_3")]
Exceptions #
Knackpy uses Python’s built-in exceptions, as well as Requests’s exceptions when interacting with the Knack API.
If you need to inspect an API exception (for example to see the text content of the response), you can access the Response
object by handling the exception like so:
>>> import requests
>>>
>>> app = knackpy.App(api_key="myappid")
# raises HTTPError. You cannot request object records without supplying an API key
>>> try:
... records = app.get("object_1")
... except requests.exceptions.HTTPError as e:
... print(e.response.text)
... raise e
# 'Unauthorized Object Access'