Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add user guide for API client class
Browse files Browse the repository at this point in the history
JWCook committed Nov 17, 2023

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent 08135b0 commit 42e7eb7
Showing 10 changed files with 186 additions and 71 deletions.
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
@@ -20,4 +20,4 @@ contributors
:maxdepth: 1
history
```
```
2 changes: 1 addition & 1 deletion docs/user_guide/advanced.md
Original file line number Diff line number Diff line change
@@ -366,4 +366,4 @@ session object used to make requests:
```python
>>> from pyinaturalist import ClientSession
>>> session = ClientSession(user_agent='my_app/1.0.0')
```
```
130 changes: 130 additions & 0 deletions docs/user_guide/client.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
(api-client)=
# API Client Class
```{warning}
These features and documentation are a work in progress!
```

In addition to the standard API wrapper functions, pyinaturalist provides {py:class}`.iNatClient`, a higher-level, object-oriented interface. It adds the following features:
* Returns fully typed {ref}`model objects <data-models>` instead of JSON
* Easier to configure
* Easier to paginate large responses
* Basic async support

## Basic usage
All API calls are available as methods on {py:class}`.iNatClient`, grouped by resource type. For example:
* Taxon requests: {py:class}`iNatClient.taxa <.TaxonController>`
* Observation requests: {py:class}`iNatClient.observations <.ObservationController>`
* These resource groups are referred to elsewhere in the docs as **controllers.** See :ref:`Controller classes <controllers>` for more info.

The main observation search is available in `client.observations.search()`.
Here is an example of searching for observations by taxon name:

```py
>>> from pyinaturalist import iNatClient
>>> client = iNatClient()
>>> observations = client.observations.search(user_id='my_username', taxon_name='Danaus plexippus').all()
```

## Pagination
Most client methods return a Paginator object. This is done so we can both:
* Make it easy to fetch multiple (or all) pages of a large request, and
* Avoid fetching more data than needed.

Paginators can be iterated over like a normal collection, and new pages will be fetched as they are needed:
```py
for obs in client.observations.search(user_id='my_username', taxon_name='Danaus plexippus'):
print(obs)
```

You can get all results at once with `.all()`:
```py
query = client.observations.search(user_id='my_username', taxon_name='Danaus plexippus')
observations = query.all()
```

Or get only up to a certain number of results with `.limit()`:
```py
observations = query.limit(500)
```

You can get only the first result with `.one()`:
```py
observation = query.one()
```

Or only the total number of results (without fetching any of them) with `.count()`:
```py
print(query.count())
```

## Single-ID requests
For most controllers, there is a shortcut to get a single object by ID, by calling the controller as a method with a single argument. For example, to get an observation by ID:
```py
observation = client.observations(12345)
```

## Authentication
Add credentials needed for {ref}`authenticated requests <auth>`:
Note: Passing credentials via environment variables or keyring is preferred

```py
>>> creds = {
... 'username': 'my_inaturalist_username',
... 'password': 'my_inaturalist_password',
... 'app_id': '33f27dc63bdf27f4ca6cd95dd9dcd5df',
... 'app_secret': 'bbce628be722bfe2abd5fc566ba83de4',
... }
>>> client = iNatClient(creds=creds)
```

## Default request parameters
There are some parameters that several different API endpoints have in common, and in some cases you may want to always use the same value. As a shortcut for this, you can pass these common parameters and their values via `default_params`.

For example, a common use case for this is to add `locale` and `preferred_place_id`:
```python
>>> default_params={'locale': 'en', 'preferred_place_id': 1}
>>> client = iNatClient(default_params=default_params)
```

These parameters will then be automatically used for any endpoints that accept them.

## Caching, Rate-limiting, and Retries
See :py:class:`.ClientSession` and :ref:`advanced` for details on these settings.

``iNatClient`` will accept any arguments for ``ClientSession``, for example:
```py
>>> client = iNatClient(per_second=50, expire_after=3600, retries=3)
```

Or you can provide your own session object:

```py
>>> session = MyCustomSession(encabulation_factor=47.2)
>>> client = iNatClient(session=session)
```

## Updating settings
All settings can also be modified after creating the client:
```py
>>> client.session = ClientSession()
>>> client.creds['username'] = 'my_inaturalist_username'
>>> client.default_params['locale'] = 'es'
>>> client.dry_run = True
```

## Async usage
Most client methods can be used in an async application without blocking the event loop. Paginator objects can be used as an async iterator:
```py
async for obs in client.observations.search(user_id='my_username'):
print(obs)
```

Or to get all results at once, use {py:meth}`Paginator.async_all`:
```py
query = client.observations.search(user_id='my_username')
observations = await query.async_all()
```


## Controller methods
This section lists all the methods available on each controller.
19 changes: 15 additions & 4 deletions docs/user_guide/general.md
Original file line number Diff line number Diff line change
@@ -158,15 +158,26 @@ ID Taxon ID Taxon Obs
(data-models)=
## Models
Data models ({py:mod}`pyinaturalist.models`) are included for all API response types. These allow
working with typed python objects instead of raw JSON. These are not used by default in the API query
functions, but you can easily use them as follows:
```python
working with typed python objects, which are generally easier to work with than raw JSON.
They provide:
* Complete type annotations and autocompletion
* Condensed print formats for easy previewing with {py:func}`~pyinaturalist.formatters.pprint` (ideal for exploring data in Jupyter)
* Almost no performance overhead (on the order of nanoseconds per object)

To use these models with the standard API query functions, you can load JSON results
with `<Model>.from_json()` (single object) or `<Model>.from_json_list()` (list of objects):
```py
>>> from pyinaturalist import Observation, get_observations
>>> response = get_observations(user_id='my_username)
>>> observations = Observation.from_json_list(response)
```

In a future release, these models will be fully integrated with the API query functions.
And they can be converted back to a JSON dict if needed:
```py
json_observations = [obs.to_dict() for obs in observations]
```

In a future release, these models will be fully integrated with API query functions. To preview these features, see {ref}`api-client`.

## API Recommended Practices
See [API Recommended Practices](https://www.inaturalist.org/pages/api+recommended+practices)
3 changes: 2 additions & 1 deletion docs/user_guide/index.md
Original file line number Diff line number Diff line change
@@ -6,4 +6,5 @@
general
advanced
```
client
```
92 changes: 31 additions & 61 deletions pyinaturalist/client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# TODO: "optional auth" option
# TODO: Improve Sphinx docs generated for controller attributes
# TODO: Use a custom template or directive to generate summary of all controller methods
from asyncio import AbstractEventLoop
from inspect import ismethod
from logging import getLogger
@@ -27,66 +28,23 @@

# 'iNatClient' is nonstandard casing, but 'InatClient' just looks wrong. Deal with it, pep8.
class iNatClient:
"""**WIP/Experimental**
"""API client class that provides an object-oriented interface to the iNaturalist API.
API client class that provides a higher-level interface that is easier to configure, and returns
:ref:`model objects <models>` instead of JSON. See :ref:`Controller classes <controllers>` for
request details.
**WIP/Experimental**
Examples:
**Basic usage**
See:
Search for observations by taxon name:
* Usage guide: :ref:`api-client`
* Query functions: :ref:`Controller classes <controllers>`
>>> from pyinaturalist import iNatClient
>>> client = iNatClient()
>>> observations = client.observations.search(taxon_name='Danaus plexippus')
Controllers:
Get a single observation by ID:
>>> observation = client.observations(12345)
**Authentication**
Add credentials needed for authenticated requests:
Note: Passing credentials via environment variables or keyring is preferred
>>> creds = {
... 'username': 'my_inaturalist_username',
... 'password': 'my_inaturalist_password',
... 'app_id': '33f27dc63bdf27f4ca6cd95dd9dcd5df',
... 'app_secret': 'bbce628be722bfe2abd5fc566ba83de4',
... }
>>> client = iNatClient(creds=creds)
**Default request parameters:**
Add default ``locale`` and ``preferred_place_id`` request params to pass to any requests
that use them:
>>> default_params={'locale': 'en', 'preferred_place_id': 1}
>>> client = iNatClient(default_params=default_params)
**Caching, Rate-limiting, and Retries**
See :py:class:`.ClientSession` and the :ref:`user_guide` for details on these settings.
``iNatClient`` will accept any arguments for ``ClientSession``, for example:
>>> client = iNatClient(per_second=50, expire_after=3600, retries=3)
Or you can provide your own custom session object:
>>> session = MyCustomSession(encabulation_factor=47.2)
>>> client = iNatClient(session=session)
**Updating settings**
All settings can also be modified after creating the client:
>>> client.session = ClientSession()
>>> client.creds['username'] = 'my_inaturalist_username'
>>> client.default_params['locale'] = 'es'
>>> client.dry_run = True
* :py:class:`annotations <.AnnotationController>`
* :py:class:`observations <.ObservationController>`
* :py:class:`places <.PlaceController>`
* :py:class:`projects <.ProjectController>`
* :py:class:`taxa <.TaxonController>`
* :py:class:`users <.UserController>`
Args:
creds: Optional arguments for :py:func:`.get_access_token`, used to get and refresh access
@@ -117,12 +75,24 @@ def __init__(
self._token_expires = None

# Controllers
self.annotations = AnnotationController(self) #: Interface for annotation requests
self.observations = ObservationController(self) #: Interface for observation requests
self.places = PlaceController(self) #: Interface for project requests
self.projects = ProjectController(self) #: Interface for project requests
self.taxa = TaxonController(self) #: Interface for taxon requests
self.users = UserController(self) #: Interface for user requests
self.annotations = AnnotationController(
self
) #: Interface for :py:class:`annotation requests <.AnnotationController>`
self.observations = ObservationController(
self
) #: Interface for :py:class:`observation requests <.ObservationController>`
self.places = PlaceController(
self
) #: Interface for :py:class:`place requests <.PlaceController>`
self.projects = ProjectController(
self
) #: Interface for :py:class:`project requests <.ProjectController>`
self.taxa = TaxonController(
self
) #: Interface for :py:class:`taxon requests <.TaxonController>`
self.users = UserController(
self
) #: Interface for :py:class:`user requests <.UserController>`

def add_client_settings(
self, request_function, kwargs: Optional[RequestParams] = None, auth: bool = False
4 changes: 3 additions & 1 deletion pyinaturalist/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Controller classes for :py:class:`.iNatClient`"""
"""Controller classes for :py:class:`.iNatClient`. These contain all the request functions used by
the client, grouped by resource type.
"""
# ruff: noqa: F401
# isort: skip_file
from pyinaturalist.controllers.base_controller import BaseController
2 changes: 1 addition & 1 deletion pyinaturalist/controllers/annotation_controller.py
Original file line number Diff line number Diff line change
@@ -9,7 +9,7 @@


class AnnotationController(BaseController):
""":fa:`tag` Controller for ControlledTerm and Annotation requests"""
""":fa:`tag` Controller for Annotation and ControlledTerm requests"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
2 changes: 1 addition & 1 deletion pyinaturalist/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Data models that represent iNaturalist API response objects.
See :ref:`user_guide:models` section for usage details.
See :ref:`data-models` section for usage details.
"""
# ruff: noqa: F401, E402
# isort: skip_file
1 change: 1 addition & 0 deletions pyinaturalist/paginator.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
"""Classes to handle pagination of API requests"""
from asyncio import AbstractEventLoop, get_running_loop
from collections import deque
from concurrent.futures import ThreadPoolExecutor

0 comments on commit 42e7eb7

Please sign in to comment.