Skip to main content

API client for FreshBooks

Project description

FreshBooks Python SDK

PyPI PyPI - Python Version GitHub Workflow Status

The FreshBooks Python SDK allows you to more easily utilize the FreshBooks API.

Installation

pip install freshbooks-sdk

Usage

See the full documentation or check out some of our examples.

Configuring the API client

You can create an instance of the API client in one of two ways:

  • By providing your application's OAuth2 client_id and client_secret and following through the auth flow, which when complete will return an access token
  • Or if you already have a valid access token, you can instantiate the client directly using that token, however token refresh flows will not function without the application id and secret.
from freshbooks import Client

freshBooksClient = Client(
    client_id=<your application id>,
    client_secret=<your application secret>,
    redirect_uri=<your redirect uri>
)

and then proceed with the auth flow (see below).

Or

from freshbooks import Client

freshBooksClient = Client(
    client_id=<your application id>,
    access_token=<a valid token>
)

Authoization flow

This is a brief summary of the OAuth2 authorization flow and the methods in the FreshBooks API Client around them. See the FreshBooks API - Authentication documentation.

First, instantiate your Client with client_id, client_secret, and redirect_uri as above.

To get an access token, the user must first authorize your application. This can be done by sending the user to the FreshBooks authorization page. Once the user has clicked accept there, they will be redirected to your redirect_uri with an access grant code. The authorization URL can be obtained by calling freshBooksClient.get_auth_request_url(). This method also accepts a list of scopes that you wish the user to authorize your application for.

auth_url = freshBooksClient.get_auth_request_url(['user:profile:read', 'user:clients:read'])

Once the user has been redirected to your redirect_uri and you have obtained the access grant code, you can exchange that code for a valid access token.

auth_results = freshBooksClient.get_access_token(access_grant_code)

This call both sets the access_token, refresh_token, and access_token_expires_at fields on you Client instance, and returns those values.

>>> auth_results.access_token
<some token>

>>> auth_results.refresh_token
<some refresh token>

>>> auth_results.access_token_expires_at
<datetime object>

When the token expires, it can be refreshed with the refresh_token value in the Client:

>>> auth_results = freshBooksClient.refresh_access_token()
>>> auth_results.access_token
<a new token>

or you can pass the refresh token yourself:

>>> auth_results = freshBooksClient.refresh_access_token(stored_refresh_token)
>>> auth_results.access_token
<a new token>

Current User

FreshBooks users are uniquely identified by their email across our entire product. One user may act on several Businesses in different ways, and our Identity model is how we keep track of it. Each unique user has an Identity, and each Identity has Business Memberships which define the permissions they have.

See FreshBooks API - Business, Roles, and Identity and FreshBooks API - The Identity Model.

The current user can be accessed by:

>>> current_user = freshBooksClient.current_user()
>>> current_user.email
<some email>

>>> current_user.business_memberships
<list of businesses>

Making API Calls

Each resource in the client provides calls for get, list, create, update and delete calls. Please note that some API resources are scoped to a FreshBooks account_id while others are scoped to a business_id. In general these fall along the lines of accounting resources vs projects/time tracking resources, but that is not precise.

client = freshBooksClient.clients.get(account_id, client_user_id)
project = freshBooksClient.projects.get(business_id, project_id)

Get and List

API calls which return a single resource return a Result object with the returned data accessible via attributes. The raw json-parsed dictionary can also be accessed via the data attribute.

client = freshBooksClient.clients.get(account_id, client_user_id)

assert client.organization == "FreshBooks"
assert client.userid == client_user_id

assert client.data["organization"] == "FreshBooks"
assert client.data["userid"] == client_user_id

vis_state returns an Enum. See FreshBooks API - Active and Deleted Objects for details.

from freshbooks import VisState

assert client.vis_state == VisState.ACTIVE
assert client.vis_state == 0
assert client.data['vis_state'] == VisState.ACTIVE
assert client.data['vis_state'] == 0

API calls which return a list of resources return a ListResult object. The resources in the list can be accessed by index and iterated over. Similarly, the raw dictionary can be accessed via the data attribute.

clients = freshBooksClient.clients.list(account_id)

assert clients[0].organization == "FreshBooks"

assert clients.data["clients"][0]["organization"] == "FreshBooks"

for client in clients:
    assert client.organization == "FreshBooks"
    assert client.data["organization"] == "FreshBooks"

Create, Update, and Delete

API calls to create and update take a dictionary of the resource data. A successful call will return a Result object as if a get call.

Create:

payload = {"email": "john.doe@abcorp.com"}
new_client = FreshBooksClient.clients.create(account_id, payload)

client_id = new_client.userid

Update:

payload = {"email": "john.doe@abcorp.ca"}
client = freshBooksClient.clients.update(account_id, client_id, payload)

assert client.email == "john.doe@abcorp.ca"

Delete:

client = freshBooksClient.clients.delete(account_id, client_id)

assert client.vis_state == VisState.DELETED

Error Handling

Calls made to the FreshBooks API with a non-2xx response are wrapped in a FreshBooksError exception. This exception class contains the error message, HTTP response code, FreshBooks-specific error number if one exists, and the HTTP response body.

Example:

from freshbooks import FreshBooksError

try:
    client = freshBooksClient.clients.get(account_id, client_id)
except FreshBooksError as e:
    assert str(e) == "Client not found."
    assert e.status_code == 404
    assert e.error_code == 1012
    assert e.raw_response ==  ("{'response': {'errors': [{'errno': 1012, "
                               "'field': 'userid', 'message': 'Client not found.', "
                               "'object': 'client', 'value': '134'}]}}")

Not all resources have full CRUD methods available. For example expense categories have list and get calls, but are not deletable. If you attempt to call a method that does not exist, the SDK will raise a FreshBooksNotImplementedError exception, but this is not something you will likely have to account for outside of development.

Pagination, Filters, and Includes

list calls take a list of builder objects that can be used to paginate, filter, and include optional data in the response. See FreshBooks API - Parameters documentation.

Pagination

Pagination results are included in list responses in the pages attribute:

>>> clients = freshBooksClient.clients.list(account_id)
>>> clients.pages
PageResult(page=1, pages=1, per_page=30, total=6)

>>> clients.pages.total
6

To make a paginated call, first create a PaginateBuilder object that can be passed into the list method.

>>> from freshbooks import PaginateBuilder

>>> paginator = PaginateBuilder(2, 4)
>>> paginator
PaginateBuilder(page=2, per_page=4)

>>> clients = freshBooksClient.clients.list(account_id, builders=[paginator])
>>> clients.pages
PageResult(page=2, pages=3, per_page=4, total=9)

PaginateBuilder has methods page and per_page to return or set the values. When setting the values the calls can be chained.

>>> paginator = PaginateBuilder(1, 3)
>>> paginator
PaginateBuilder(page=1, per_page=3)

>>> paginator.page()
1

>>> paginator.page(2).per_page(4)
>>> paginator
PaginateBuilder(page=2, per_page=4)

ListResults can be combined, allowing your to use pagination to get all the results of a resource.

paginator = PaginateBuilder(1, 100)
clients = freshBooksClient.clients.list(self.account_id, builders=[paginator])
while clients.pages.page < clients.pages.pages:
    paginator.page(clients.pages.page + 1)
    new_clients = freshBooksClient.clients.list(self.account_id, builders=[paginator])
    clients = clients + new_clients
Filters

To filter which results are return by list method calls, construct a FilterBuilder and pass that in the list of builders to the list method.

>>> from freshbooks import FilterBuilder

>>> filter = FilterBuilder()
>>> filter.equals("userid", 123)

>>> clients = freshBooksClient.clients.list(account_id, builders=[filter])

Filters can be built with the methods: equals, in_list, like, between, and boolean, which can be chained together.

>>> f = FilterBuilder()
>>> f.like("email_like", "@freshbooks.com")
FilterBuilder(&search[email_like]=@freshbooks.com)

>>> f = FilterBuilder()
>>> f.in_list("clientids", [123, 456]).boolean("active", False)
FilterBuilder(&search[clientids][]=123&search[clientids][]=456&active=False)

>>> f = FilterBuilder()
>>> f.boolean("active", False).in_list("clientids", [123, 456])
FilterBuilder(&active=False&search[clientids][]=123&search[clientids][]=456)

>>> f = FilterBuilder()
>>> f.between("amount", 1, 10)
FilterBuilder(&search[amount_min]=1&search[amount_max]=10)

>>> f = FilterBuilder()
>>> f.between("start_date", date.today())
FilterBuilder(&search[start_date]=2020-11-21)
Includes

To include additional relationships, sub-resources, or data in a response an IncludesBuilder can be constructed.

>>> from freshbooks import IncludesBuilder

>>> includes = IncludesBuilder()
>>> includes.include("outstanding_balance")
IncludesBuilder(&include[]=outstanding_balance)

Which can then be passed into list or get calls:

>>> clients = freshBooksClient.clients.list(account_id, builders=[includes])
>>> clients[0].outstanding_balance
[{'amount': {'amount': '100.00', 'code': 'USD'}}]

>>> client = freshBooksClient.clients.get(account_id, client_id, includes=includes)
>>> client.outstanding_balance
[{'amount': {'amount': '100.00', 'code': 'USD'}}]

Includes can also be passed into create and update calls to include the data in the response of the updated resource:

>>> payload = {"email": "john.doe@abcorp.com"}
>>> new_client = FreshBooksClient.clients.create(account_id, payload, includes=includes)
>>> new_client.outstanding_balance
[]  # New client has no balance
Sorting

TODO:

Dates and Times

For historical reasons, some resources in the FreshBooks API (mostly accounting-releated) return date/times in "US/Eastern" timezone. Some effort is taken to return datetime objects as zone-aware and normalized to UTC. In these cases, the raw response string will differ from the attribute. For example:

from datetime import datetime, timezone

assert client.data["updated"] == "2021-04-16 10:31:59"  # Zone-naive string in "US/Eastern"
assert client.updated.isoformat() == '2021-04-16T14:31:59+00:00'  # Zone-aware datetime in UTC
assert client.updated == datetime(year=2021, month=4, day=16, hour=14, minute=31, second=59, tzinfo=timezone.utc)

Development

Testing

To run all tests:

make test

To run a single test with pytest:

py.test path/to/test/file.py
py.test path/to/test/file.py::TestClass::test_case

Documentations

You can generate the documentation via:

make generate-docs

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

freshbooks-sdk-1.0.1.tar.gz (24.6 kB view hashes)

Uploaded source

Built Distribution

freshbooks_sdk-1.0.1-py2.py3-none-any.whl (29.4 kB view hashes)

Uploaded py2 py3

Supported by

AWS AWS Cloud computing Datadog Datadog Monitoring Facebook / Instagram Facebook / Instagram PSF Sponsor Fastly Fastly CDN Google Google Object Storage and Download Analytics Huawei Huawei PSF Sponsor Microsoft Microsoft PSF Sponsor NVIDIA NVIDIA PSF Sponsor Pingdom Pingdom Monitoring Salesforce Salesforce PSF Sponsor Sentry Sentry Error logging StatusPage StatusPage Status page