Skip to content

Repository files navigation

fulcrum-python

Build Status PyPI version

A library for working with Fulcrum API

What's new in 2.0

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), and sketches.
  • Fuller CRUD on memberships: find, create, update, delete, and for_object(type, object_id), in addition to the existing search and change.
  • History endpoints: forms.history(id), records.history(id), and records.all_history().
  • records.partial_update(id, obj) for PATCH support.
  • Data residency regions: pass region=FulcrumRegion.EU (or .AU, .CA, .US) instead of a raw uri string.
  • Better error handling: every non-2xx response now raises a typed exception (fulcrum.exceptions.ForbiddenException, UnprocessableEntityException, etc.) carrying .status_code and .response_body, instead of silently returning whatever the API sent back for status codes the 1.x client didn't recognize.
  • on_request hook: 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 FulcrumClient alias for the Fulcrum class, 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 generic media() method can't reach.

Backwards compatibility

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.

Installation

Install via pip:

pip install fulcrum

or from local sources:

python setup.py install

Dependencies

Just one - Requests takes care of our HTTP chatting, and is automatically installed when using the steps above.

Supported Resources and Methods

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

Usage

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.

Forms

fulcrum.forms.find(id)

fulcrum.forms.search(url_params)

fulcrum.forms.create(form)

fulcrum.forms.update(id, form)

fulcrum.forms.delete(id)

Records

fulcrum.records.find(id)

fulcrum.records.search(url_params)

fulcrum.records.create(record)

fulcrum.records.update(id, record)

fulcrum.records.delete(id)

Photos

fulcrum.photos.find(id)

fulcrum.photos.search(url_params)

fulcrum.photos.media(id, size)

fulcrum.photos.create(media_or_path, content_type='image/jpeg', access_key=None)

Signatures

fulcrum.signatures.find(id)

fulcrum.signatures.search(url_params)

fulcrum.signatures.media(id, size)

fulcrum.signatures.create(media_or_path, content_type='image/png', access_key=None)

Videos

fulcrum.videos.find(id)

fulcrum.videos.search(url_params)

fulcrum.videos.media(id, size)

fulcrum.videos.track(id, format)

fulcrum.videos.create(media_or_path, content_type='video/mp4', access_key=None)

Audio

fulcrum.audio.find(id)

fulcrum.audio.search(url_params)

fulcrum.audio.media(id, size)

fulcrum.audio.track(id, format)

fulcrum.audio.create(media_or_path, content_type='audio/mp3', access_key=None)

Projects

fulcrum.projects.find(id)

fulcrum.projects.search(url_params)

fulcrum.projects.create(form)

fulcrum.projects.update(id, form)

fulcrum.projects.delete(id)

Choice Lists

fulcrum.choice_lists.find(id)

fulcrum.choice_lists.search(url_params)

fulcrum.choice_lists.create(form)

fulcrum.choice_lists.update(id, form)

fulcrum.choice_lists.delete(id)

Classification Sets

fulcrum.classification_sets.find(id)

fulcrum.classification_sets.search(url_params)

fulcrum.classification_sets.create(form)

fulcrum.classification_sets.update(id, form)

fulcrum.classification_sets.delete(id)

Webhooks

fulcrum.webhooks.find(id)

fulcrum.webhooks.search(url_params)

fulcrum.webhooks.create(form)

fulcrum.webhooks.update(id, form)

fulcrum.webhooks.delete(id)

Layers

fulcrum.layers.find(id)

fulcrum.layers.search(url_params)

fulcrum.layers.create(form)

fulcrum.layers.update(id, form)

fulcrum.layers.delete(id)

Changesets

fulcrum.changesets.find(id)

fulcrum.changesets.search(url_params)

fulcrum.changesets.create(form)

fulcrum.changesets.update(id, form)

fulcrum.changesets.close(id)

Memberships

fulcrum.memberships.search(url_params)

Roles

fulcrum.roles.search(url_params)

Common Methods

Find

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 Food

Search

Search 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-524446d63f40

Create

Create 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-3cba95af0b4b

Update

Update 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 value

Delete

Delete 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.NotFoundException

Media Method

The 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)

Track Method

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)

Media Creation

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)

Query API

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]}}]}

New Resources (2.0)

# 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)

Examples

https://github.com/fulcrumapp/fulcrum-python/wiki/Examples

Hacking

Set up a virtual environment and source it:

python -m venv my_venv
source my_env/bin/activate

Install dependencies:

python setup.py install

Testing

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

Publishing

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 version

Commit 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/*

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages