pyinaturalist

https://badge.fury.io/py/pyinaturalist.png https://www.travis-ci.com/niconoe/pyinaturalist.svg?branch=master Documentation Status

Python client for the iNaturalist APIs.

Status

Work in progress: features are implemented one by one, as time allows and as the authors needs them.

That being said, many things are already possible (searching observations, creating a new observation, …) and contributions are welcome!

Python 3 only.

Examples

Search all observations matching a criteria:

from pyinaturalist.node_api import get_all_observations

obs = get_all_observations(params={'user_id': 'niconoe'})

see available parameters.

For authenticated API calls, you first need to obtain a token for the user:

from pyinaturalist.rest_api import get_access_token

token = get_access_token(username='<your_inaturalist_username>', password='<your_inaturalist_password>',
                         app_id='<your_inaturalist_app_id>',
                         app_secret=<your_inaturalist_app_secret>)

Note: you’ll need to create an iNaturalist app.

Create a new observation:

from pyinaturalist.rest_api import create_observations

params = {'observation':
            {'taxon_id': 54327,  # Vespa Crabro
             'observed_on_string': datetime.datetime.now().isoformat(),
             'time_zone': 'Brussels',
             'description': 'This is a free text comment for the observation',
             'tag_list': 'wasp, Belgium',
             'latitude': 50.647143,
             'longitude': 4.360216,
             'positional_accuracy': 50, # meters,

             # sets vespawatch_id (an observation field whose ID is 9613) to the value '100'.
             'observation_field_values_attributes':
                [{'observation_field_id': 9613,'value': 100}],
             },
}

r = create_observations(params=params, access_token=token)

new_observation_id = r[0]['id']

Upload a picture for this observation:

from pyinaturalist.rest_api import add_photo_to_observation

r = add_photo_to_observation(observation_id=new_observation_id,
                             file_object=open('/Users/nicolasnoe/vespa.jpg', 'rb'),
                             access_token=token)

Update an existing observation of yours:

from pyinaturalist.rest_api import update_observation

p = {'ignore_photos': 1,  # Otherwise existing pictures will be deleted
     'observation': {'description': 'updated description !'}}
r = update_observation(observation_id=17932425, params=p, access_token=token)

Get a list of all (globally available) observation fields:

from pyinaturalist.rest_api import get_all_observation_fields

r = get_all_observation_fields(search_query="DNA")

Sets an observation field value to an existing observation:

from pyinaturalist.rest_api import put_observation_field_values

put_observation_field_values(observation_id=7345179,
                             observation_field_id=9613,
                             value=250,
                             access_token=token)

Contents:

Installation

Simply use pip:

$ pip install pyinaturalist

Or if you prefer using the development version:

$ pip install git+https://github.com/niconoe/pyinaturalist.git

Or, to set up for local development (preferably in a new virtualenv):

$ git clone https://github.com/niconoe/pyinaturalist.git
$ cd pyinaturalist
$ pip install -Ue ".[dev]"

Reference

iNaturalist actually provides two APIs:

  • the REST API that they also use internally: it is very complete and provides read/write access, but is rather slow and sometimes inconsistent.

  • the Node-based API allows searching and returning core data, is faster and provides more consistent returned results than the REST API, but has less features.

Pyinaturalist provides functions to use both of those APIs.

Note

While not mandatory, it is considered good practice in the iNaturalist community to set a custom user-agent header to your API calls. That allows iNaturalist to identify “who’s doing what” with their APIs, and maybe contact you back in case they want to start a discussion about how you use them.

It is recommended to set this user-agent field to either something that identifies the project (MyCoolAndroidApp/2.0) or its contact person (Jane Doe, iNat user XXXXXX, jane@doe.net).

Pyinaturalist therefore provides a couple of features to make that easy:

import pyinaturalist
from pyinaturalist.node_api import get_observation

pyinaturalist.user_agent = "MyCoolAndroidApp/2.0 (using Pyinaturalist)"

# From now on, all API calls will use this user-agent.

t = get_access_token('username', 'password', 'app_id', 'app_secret')
do_something_else()
get_observation(observation_id=1234)
...

In the rare cases where you want to use multiple user agents in your script, you can configure it per call:

get_observation(observation_id=16227955, user_agent='AnotherUserAgent')

(All functions that communicate with the API accept the user_agent optional parameter).

If you don’t configure the user agent, Pyinaturalist/<VERSION> will be used.

REST API
pyinaturalist.rest_api.add_photo_to_observation(observation_id, file_object, access_token, user_agent=None)[source]

Upload a picture and assign it to an existing observation.

Parameters
  • observation_id (int) – the ID of the observation

  • file_object (BinaryIO) – a file-like object for the picture. Example: open(‘/Users/nicolasnoe/vespa.jpg’, ‘rb’)

  • access_token (str) – the access token, as returned by get_access_token()

  • user_agent (Optional[str]) – a user-agent string that will be passed to iNaturalist.

pyinaturalist.rest_api.create_observations(params, access_token, user_agent=None)[source]

Create a single or several (if passed an array) observations).

Parameters
  • params (Dict[str, Dict[str, Any]]) –

  • access_token (str) – the access token, as returned by get_access_token()

  • user_agent (Optional[str]) – a user-agent string that will be passed to iNaturalist.

Return type

List[Dict[str, Any]]

Returns

iNaturalist’s JSON response, as a Python object

Raise

requests.HTTPError, if the call is not successful. iNaturalist returns an error 422 (unprocessable entity) if it rejects the observation data (for example an observation date in the future or a latitude > 90. In that case the exception’s response attribute give details about the errors.

allowed params: see https://www.inaturalist.org/pages/api+reference#post-observations

Example:

params = {‘observation’:

{‘species_guess’: ‘Pieris rapae’},

}

TODO investigate: according to the doc, we should be able to pass multiple observations (in an array, and in renaming observation to observations, but as far as I saw they are not created (while a status of 200 is returned)

pyinaturalist.rest_api.delete_observation(observation_id, access_token, user_agent=None)[source]

Delete an observation.

Parameters
  • observation_id (int) –

  • access_token (str) –

  • user_agent (Optional[str]) – a user-agent string that will be passed to iNaturalist.

Return type

List[Dict[str, Any]]

Returns

iNaturalist’s JSON response, as a Python object (currently raise a JSONDecodeError because of an iNaturalist bug

Raise

ObservationNotFound if the requested observation doesn’t exists, requests.HTTPError (403) if the observation belongs to another user

pyinaturalist.rest_api.get_access_token(username, password, app_id, app_secret, user_agent=None)[source]

Get an access token using the user’s iNaturalist username and password.

(you still need an iNaturalist app to do this)

Parameters
  • username (str) –

  • password (str) –

  • app_id (str) –

  • app_secret (str) –

  • user_agent (Optional[str]) – a user-agent string that will be passed to iNaturalist.

Return type

str

Returns

the access token, example use: headers = {“Authorization”: “Bearer %s” % access_token}

pyinaturalist.rest_api.get_all_observation_fields(search_query='', user_agent=None)[source]

Like get_observation_fields(), but handles pagination for you.

Parameters
  • search_query (str) – a string to search

  • user_agent (Optional[str]) – a user-agent string that will be passed to iNaturalist.

Return type

List[Dict[str, Any]]

pyinaturalist.rest_api.get_observation_fields(search_query='', page=1, user_agent=None)[source]

Search the (globally available) observation

Parameters
  • search_query (str) –

  • page (int) –

  • user_agent (Optional[str]) – a user-agent string that will be passed to iNaturalist.

Return type

List[Dict[str, Any]]

Returns

pyinaturalist.rest_api.put_observation_field_values(observation_id, observation_field_id, value, access_token, user_agent=None)[source]

Sets an observation field (value) on an observation.

Parameters
  • observation_id (int) –

  • observation_field_id (int) –

  • value (Any) –

  • access_token (str) – access_token: the access token, as returned by get_access_token()

  • user_agent (Optional[str]) – a user-agent string that will be passed to iNaturalist.

Return type

Dict[str, Any]

Returns

iNaturalist’s response as a dict, for example: {‘id’: 31,

’observation_id’: 18166477, ‘observation_field_id’: 31, ‘value’: ‘fouraging’, ‘created_at’: ‘2012-09-29T11:05:44.935+02:00’, ‘updated_at’: ‘2018-11-13T10:49:47.985+01:00’, ‘user_id’: 1, ‘updater_id’: 1263313, ‘uuid’: ‘b404b654-1bf0-4299-9288-52eeda7ac0db’, ‘created_at_utc’: ‘2012-09-29T09:05:44.935Z’, ‘updated_at_utc’: ‘2018-11-13T09:49:47.985Z’}

Will fail if this observation_field is already set for this observation.

pyinaturalist.rest_api.update_observation(observation_id, params, access_token, user_agent=None)[source]

Update a single observation. See https://www.inaturalist.org/pages/api+reference#put-observations-id

Parameters
  • observation_id (int) – the ID of the observation to update

  • params (Dict[str, Any]) – to be passed to iNaturalist API

  • access_token (str) – the access token, as returned by get_access_token()

  • user_agent (Optional[str]) – a user-agent string that will be passed to iNaturalist.

Return type

List[Dict[str, Any]]

Returns

iNaturalist’s JSON response, as a Python object

Raise

requests.HTTPError, if the call is not successful. iNaturalist returns an error 410 if the observation doesn’t exists or belongs to another user (as of November 2018).

Node-based API
pyinaturalist.node_api.get_all_observations(params, user_agent=None)[source]

Like get_observations() but handles pagination so you get all the results in one shot.

Some params will be overwritten: order_by, order, per_page, id_above (do NOT specify page when using this).

Returns a list of dicts (one entry per observation)

Return type

List[Dict[str, Any]]

pyinaturalist.node_api.get_observation(observation_id, user_agent=None)[source]

Get details about an observation.

Parameters
  • observation_id (int) –

  • user_agent (Optional[str]) – a user-agent string that will be passed to iNaturalist.

Return type

Dict[str, Any]

Returns

a dict with details on the observation

Raises

ObservationNotFound

pyinaturalist.node_api.get_observations(params, user_agent=None)[source]

Search observations, see: http://api.inaturalist.org/v1/docs/#!/Observations/get_observations.

Returns the parsed JSON returned by iNaturalist (observations in r[‘results’], a list of dicts)

Return type

Dict[str, Any]

pyinaturalist.node_api.get_rank_range(min_rank=None, max_rank=None)[source]

Translate min and/or max rank into a list of ranks

Return type

List[str]

pyinaturalist.node_api.get_taxa(user_agent=None, min_rank=None, max_rank=None, **params)[source]

Given zero to many of following parameters, returns taxa matching the search criteria. See https://api.inaturalist.org/v1/docs/#!/Taxa/get_taxa

Parameters
  • q – Name must begin with this value

  • is_active – Taxon is active

  • taxon_id – Only show taxa with this ID, or its descendants

  • parent_id – Taxon’s parent must have this ID

  • rank – Taxon must have this exact rank

  • min_rank (Optional[str]) – Taxon must have this rank or higher; overrides rank

  • max_rank (Optional[str]) – Taxon must have this rank or lower; overrides rank

  • rank_level – Taxon must have this rank level. Some example values are 70 (kingdom), 60 (phylum), 50 (class), 40 (order), 30 (family), 20 (genus), 10 (species), 5 (subspecies)

  • id_above – Must have an ID above this value

  • id_below – Must have an ID below this value

  • per_page – Number of results to return in a page. The maximum value is generally 200 unless otherwise noted

  • locale – Locale preference for taxon common names

  • preferred_place_id – Place preference for regional taxon common names

  • only_id – Return only the record IDs

  • all_names – Include all taxon names in the response

Return type

Dict[str, Any]

Returns

A list of dicts containing taxa results

pyinaturalist.node_api.get_taxa_autocomplete(user_agent=None, **params)[source]

Given a query string, returns taxa with names starting with the search term See: https://api.inaturalist.org/v1/docs/#!/Taxa/get_taxa_autocomplete

Parameters
  • q – Name must begin with this value

  • is_active – Taxon is active

  • taxon_id – Only show taxa with this ID, or its descendants

  • rank – Taxon must have this rank

  • rank_level – Taxon must have this rank level. Some example values are 70 (kingdom), 60 (phylum), 50 (class), 40 (order), 30 (family), 20 (genus), 10 (species), 5 (subspecies)

  • per_page – Number of results to return in a page. The maximum value is generally 200 unless otherwise noted

  • locale – Locale preference for taxon common names

  • preferred_place_id – Place preference for regional taxon common names

  • all_names – Include all taxon names in the response

Return type

Dict[str, Any]

Returns

A list of dicts containing taxa results

pyinaturalist.node_api.get_taxa_by_id(taxon_id, user_agent=None)[source]

Get one or more taxa by ID. See: https://api.inaturalist.org/v1/docs/#!/Taxa/get_taxa_id

Param

taxon_id: Get taxa with this ID. Multiple values are allowed.

Return type

Dict[str, Any]

Returns

A list of dicts containing taxa results

pyinaturalist.node_api.make_inaturalist_api_get_call(endpoint, params, user_agent=None, **kwargs)[source]

Make an API call to iNaturalist.

endpoint is a string such as ‘observations’ method: ‘GET’, ‘HEAD’, ‘POST’, ‘PUT’, ‘PATCH’, ‘DELETE’ kwargs are passed to requests.request Returns a requests.Response object

Return type

Response

Exceptions
exception pyinaturalist.exceptions.AuthenticationError[source]

Bases: Exception

exception pyinaturalist.exceptions.ObservationNotFound[source]

Bases: Exception

Contributing

Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given.

You can contribute in many ways:

Types of Contributions
Report Bugs

Report bugs at https://github.com/niconoe/pyinaturalist/issues.

If you are reporting a bug, please include:

  • Your operating system name and version.

  • Any details about your local setup that might be helpful in troubleshooting.

  • Detailed steps to reproduce the bug.

Fix Bugs

Look through the GitHub issues for bugs. Anything tagged with “bug” is open to whoever wants to implement it.

Implement Features

Look through the GitHub issues for features. Anything tagged with “feature” is open to whoever wants to implement it.

Write Documentation

pyinaturalist could always use more documentation, whether as part of the official pyinaturalist docs, in docstrings, or even on the web in blog posts, articles, and such.

Submit Feedback

The best way to send feedback is to file an issue at https://github.com/niconoe/pyinaturalist/issues.

If you are proposing a feature:

  • Explain in detail how it would work.

  • Keep the scope as narrow as possible, to make it easier to implement.

  • Remember that this is a volunteer-driven project, and that contributions are welcome :)

Infrastructure

Documentation

We use Sphinx, and the references page is automatically generated thanks to sphinx.ext.autodoc and sphinx_autodoc_typehints extensions. All functions / methods / classes should have a proper docstring.

To build the doc locally:

$ tox -e docs

To preview:

# MacOS:
$ open docs/_build/index.html
# Linux:
$ xdg-open docs/_build/index.html

Hosted documentation (https://pyinaturalist.readthedocs.io/) is automatically updated when code gets pushed to GitHub.

Testing

We use the pytest framework.

To run locally:

$ pytest

It is however always good to run tox frequently, to run the tests against multiple Python versions, as well as some style and type annotations checks:

$ tox

Travis-CI is run when code is pushed to GitHub.

Type annotations

All functions / methods should have parameters and return value type annotations. Those type annotations are checked by MyPy (tox -e mypy) and will appear in the documentation.

Releasing at PyPI

Release checklist:

  • Make sure the code is tested, annotated and documented.

  • Update version in HISTORY.rst, setup.py and pyinaturalist/__init__.py

  • Create the distributions: python setup.py sdist bdist_wheel

  • Use twine to upload the package to PyPI: twine upload dist/*

  • Push a vX.Y.Z tag to GitHub: git tag vX.Y.Z && git push origin --tags

Credits

Development Lead
Contributors
  • Jordan Cook

  • Peter Desmet

  • Stijn Van Hoey

History

0.8.0 (2019-07-11)
  • all functions now take an optional user-agent parameter in order to identify yourself to iNaturalist. If not set, Pyinaturalist/<VERSION> will be used.

0.7.0 (2019-05-08)
  • rest_api.delete_observation() now raises ObservationNotFound if the observation doesn’t exists

  • minor dependencies update for security reasons

0.6.0 (2018-11-15)
  • New function: rest_api.delete_observation()

0.5.0 (2018-11-05)
  • New function: node_api.get_observation()

0.4.0 (2018-11-05)
  • create_observation() now raises exceptions in case of errors.

0.3.0 (2018-11-05)
  • update_observation() now raises exceptions in case of errors.

0.2.0 (2018-10-31)
  • Better infrastructure (type annotations, documentation, …)

  • Dropped support for Python 2.

  • New function: update_observation()

  • rest_api.AuthenticationError is now exceptions.AuthenticationError

0.1.0 (2018-10-10)
  • First release on PyPI.

Feedback

If you have any suggestions or questions about pyinaturalist feel free to email me at nicolas@niconoe.eu.

If you encounter any errors or problems with pyinaturalist, please let me know! Open an Issue at the GitHub http://github.com/niconoe/pyinaturalist main repository.