A library for working with Fulcrum API
This release brings the library up to parity with the current Fulcrum API and with fulcrum-js. Everything from 1.x still works exactly as before (see "Backwards compatibility" below) - this is additive, not a breaking rewrite.
- New resources:
groups,workflows,report_templates,batches,attachments(generic upload flow),users,reports(ad hoc report generation), andsketches. - Fuller CRUD on
memberships:find,create,update,delete, andfor_object(type, object_id), in addition to the existingsearchandchange. - History endpoints:
forms.history(id),records.history(id), andrecords.all_history(). records.partial_update(id, obj)forPATCHsupport.- Data residency regions: pass
region=FulcrumRegion.EU(or.AU,.CA,.US) instead of a rawuristring. - Better error handling: every non-2xx response now raises a typed
exception (
fulcrum.exceptions.ForbiddenException,UnprocessableEntityException, etc.) carrying.status_codeand.response_body, instead of silently returning whatever the API sent back for status codes the 1.x client didn't recognize. on_requesthook:Fulcrum(key='...', on_request=lambda method, path, response: ...)for wiring up your own logging/tracing (fulcrum-js uses OpenTelemetry for the equivalent purpose; this library stays dependency-free).- A
FulcrumClientalias for theFulcrumclass, for anyone porting code or examples over from fulcrum-js. - Fixed:
signatures.media()only supports a'thumbnail'size (there is no'large'signature variant in the API - the 1.x library's docs and size list were wrong here). - Added
videos.thumbnail(id, size='medium', square=False)for the eight video thumbnail variants the API exposes (small/medium/large/huge, each with a square option), which the genericmedia()method can't reach.
If you're upgrading from 1.x: nothing you're already calling changes. Every
1.x method, resource, and exception class is still there with the same name
and the same behavior, including the historically-undocumented
memberships.change(...) helper and authorizations.regenerate(id). New
functionality is additive. The one intentional behavior change is the error
handling improvement above - if your code was relying on a 403 or 422 response
silently not raising, it will now raise ForbiddenException /
UnprocessableEntityException instead.
Install via pip:
pip install fulcrum
or from local sources:
python setup.py install
Just one - Requests takes care of our HTTP chatting, and is automatically installed when using the steps above.
| Resource | Methods |
|---|---|
| Forms | find, search, create, update, delete, history |
| Records | find, search, create, update, delete, partial_update, history, all_history |
| Photos | find, search, media, create |
| Signatures | find, search, media, create |
| Sketches | find, search, media, create |
| Projects | find, search, create, update, delete |
| Changesets | find, search, create, update, close |
| Choice Lists | find, search, create, update, delete |
| Classification Sets | find, search, create, update, delete |
| Webhooks | find, search, create, update, delete |
| Layers | find, search, create, update, delete |
| Videos | find, search, media, thumbnail, track, create |
| Audio | find, search, media, track, create |
| Memberships | search, find, create, update, delete, change, change_permissions, for_object |
| Roles | search |
| Audit Logs | search, find |
| Authorizations | find, search, update, delete, regenerate (deprecated) |
| Groups | find, search, create, update, delete, get_resource, change_permissions |
| Workflows | find, search, create, update, delete |
| Report Templates | find, search, create, update, delete |
| Batches | find, search, create, add_operations, start |
| Attachments | find, search, create, delete, finalize, copy_all |
| Users | get |
| Reports | create, file |
Create a fulcrum client with your API key.
from fulcrum import Fulcrum
fulcrum = Fulcrum(key='super-secret-key')Various methods are available for each of the resources. Results are returned as python-equivalent dicts of the JSON returned from the API. Check the Fulcrum API Docs for examples of returned objects.
Finds a single resource. The single parameter is a resource id.
form = fulcrum.forms.find('5b656cd8-f3ef-43e9-8d22-84d015052778')
print(form['form']['name']) # Denver Street FoodSearch for resources. The single parameter is url_params which should be passed as a dict, and will be properly url encoded. These will vary depending on the resource, but pagination parameters are always accepted.
records = fulcrum.records.search(url_params={'form_id': 'a1cb3ac7-146f-491a-a4a2-47737fb12074'})
print(len(records['records'])) # 9
print(records['records'][0]['id']) # c90b0edf-0299-42df-bed4-524446d63f40Create an object. The single parameter is a dict representation of a JSON object that will be POSTed to the API. Check the Fulcrum API Docs for examples of resource objects.
a_record = {
'record': {
'form_values': {
'cbaf': 'A field value'
},
'form_id': 'a1cb3ac7-146f-491a-a4a2-47737fb12074'
}
}
record = fulcrum.records.create(a_record)
print(record['record']['id']) # e58e80a8-9376-4a31-8e31-3cba95af0b4bUpdate an object. Parameters are an id, and dict representation of the JSON object that will be updated.
an_updated_record = {
'record': {
'form_values': {
'cbaf': 'An updated field value'
},
'form_id': 'a1cb3ac7-146f-491a-a4a2-47737fb12074'
}
}
record = fulcrum.records.update('e58e80a8-9376-4a31-8e31-3cba95af0b4b', an_updated_record)
print(record['record']['form_values']['cbaf']) # An updated field valueDelete a resource. Delete returns None on success and raises fulcrum.exceptions.NotFoundException if the API returns a 404 (no resource found).
fulcrum.records.delete('e58e80a8-9376-4a31-8e31-3cba95af0b4b') # Returns None (assuming the record is found and deleted)
fulcrum.records.delete('a-bogus-resource-id') # Raises fulcrum.exceptions.NotFoundExceptionThe Fulcrum API endpoints that support media download have an extra media
method that will fetch the raw media.
The size options are:
| Resource | Sizes |
|---|---|
| Photos | 'original', 'thumbnail', and 'large' |
| Signatures | 'original' and 'thumbnail' (there is no 'large' variant for signatures) |
| Sketches | 'original', 'thumbnail', and 'large' |
| Videos | 'original', 'small', and 'medium' via media(); use videos.thumbnail(id, size, square) for the thumbnail variants ('small'/'medium'/'large'/'huge', each optionally square) |
| Audio | 'original' |
photo = fulcrum.photos.media(id, size='original')Skip the size parameter and get the default, original photo. Save it to disk.
photo = fulcrum.photos.media('e58e80a8-9376-4a31-8e31-3cba95af0b4b')
with open('photo_original.jpg', 'wb') as f:
f.write(photo)Get the thumbnail instead.
photo = fulcrum.photos.media('e58e80a8-9376-4a31-8e31-3cba95af0b4b', 'thumbnail')
with open('photo_thumb.jpg', 'wb') as f:
f.write(photo)Do the same with videos.
video = fulcrum.videos.media('45f85af9-65d1-4356-b8d1-6e713e926c22', 'small')
with open('video_small.mp4', 'wb') as f:
f.write(video)The audio and video endpoints have an extra track method that will fetch a
track associated with the recording in multiple formats: json (default),
geojson, gpx, and kml.
Get the default json track output for an audio recording.
track = fulcrum.audio.track('f0eb217d-3d4b-4ade-81b7-bac63788f396')
with open('track.json', 'w') as f:
f.write(track)Get a KML representation of a video track.
track = fulcrum.videos.track('45f85af9-65d1-4356-b8d1-6e713e926c22', 'kml')
with open('track.kml', 'w') as f:
f.write(track)Photos, videos, audio, and signatures can also be created. A single argument is
required: A path to the file, or a file object (via open('file_name.ext')).
photo_path = 'door.jpg'
photo_resp = fulcrum.photos.create(photo_path)
video = open('video.mp4', 'rb')
video_resp = fulcrum.videos.create(video)You can specifiy the file content type if you have a file type different than the default.
image_path = 'site_plan.png'
resp = fulcrum.photos.create(image_path, content_type='image/png')IDs (access keys) are created automatically for new media objects, but you can specify your own too.
access_key = 'a743718b-8e62-484b-bbf3-600f5055a636'
audio_path = 'audio_recording.mp3'
resp = fulcrum.audio.create(audio_path, access_key=access_key)The client object has a query method that can be used to access the Query API. The arguments are a SQL string, and an optional format. The default format is 'json'. Other formats are 'csv' or 'geojson'.
# Get JSON, the default format.
as_json = fulcrum.query('SELECT * FROM Expenses LIMIT 1;')
print(as_json)
# {'fields': [{'name': '_record_id', 'type': 'string'}], 'time': 0.01, 'date': 1539895212724}
# CSV is cool too.
as_csv = fulcrum.query('SELECT * FROM Expenses LIMIT 1;', 'csv')
print(as_csv)
# '_record_id,_project_id,_assigned_to_id\nabc,123,def\n'
# Or get some GeoJSON.
as_geojson = fulcrum.query('SELECT * FROM Expenses LIMIT 1;', 'geojson')
print(as_geojson)
# {'type': 'FeatureCollection', 'features': [{'type': 'Feature', 'properties': {'_record_id': 'abc', '_project_id': '123'}, 'geometry': {'type': 'Point', 'coordinates': [-82.63707, 27.77102]}}]}# Groups
group = fulcrum.groups.create({'group': {'name': 'Field Crew'}})
members = fulcrum.groups.get_resource(group['group']['id'], 'members')
fulcrum.groups.change_permissions({'type': 'group_members', 'group_id': group['group']['id'], 'add': ['user-id']})
# Workflows / Report Templates - standard find/search/create/update/delete
workflow = fulcrum.workflows.create({'workflow': {'name': 'Auto-assign'}})
template = fulcrum.report_templates.create({'report_template': {'name': 'Inspection Report', 'form_id': 'f1'}})
# Batches
batch = fulcrum.batches.create({'batch': {'name': 'Bulk import'}})
fulcrum.batches.add_operations(batch['batch']['id'], [{'type': 'create', 'resource': {...}}])
fulcrum.batches.start(batch['batch']['id'])
# Attachments (generic upload flow)
attachment = fulcrum.attachments.create({'attachment': {'owners': [{'type': 'record', 'record_id': 'r1', 'field_key': 'abc'}]}})
fulcrum.attachments.finalize({'attachment_id': attachment['attachment']['id']})
# Users
me = fulcrum.users.get()
# Reports (ad hoc, not tied to a saved report template)
report = fulcrum.reports.create({'report': {'form_id': 'f1', 'format': 'pdf'}})
pdf_bytes = fulcrum.reports.file(report['report']['id'])
# Memberships - full CRUD, plus listing permissions for a form/project/layer
membership = fulcrum.memberships.create({'membership': {'form_id': 'f1', 'user_id': 'u1', 'role': 'editor'}})
fulcrum.memberships.for_object('form', 'f1')
# History
fulcrum.records.history('r1')
fulcrum.records.all_history()
fulcrum.forms.history('f1')
# Regions
from fulcrum import Fulcrum, FulcrumRegion
fulcrum_eu = Fulcrum(key='super-secret-key', region=FulcrumRegion.EU)
# Request hook (logging/tracing)
def log_request(method, path, response):
print(method.upper(), path, response.status_code)
fulcrum = Fulcrum(key='super-secret-key', on_request=log_request)https://github.com/fulcrumapp/fulcrum-python/wiki/Examples
Set up a virtual environment and source it:
python -m venv my_venv
source my_env/bin/activate
Install dependencies:
python setup.py install
You'll need some additional things to run tests, so:
pip install -r test_requirements.txt
Run the tests:
python -m unittest discover
Or, with pytest and coverage:
pip install pytest pytest-cov
pytest --cov=fulcrum
Bump the version in setup.py and __init__.py:
# setup.py
setup(
name='fulcrum',
version='1.10.0', # The next version
...
)# __init__.py
__version__ = '1.10.0' # The next versionCommit the changes above, tag, and push:
git commit -am "Bump to version 1.10.0" git tag -a v1.10.0 -m "version 1.10.0" git push && git push --tags
Install the twine dependency:
pip install twine
Then, package it up and upload:
python setup.py sdist
twine upload dist/*
