Skip to content

Commit

Permalink
Add user guide for API client class
Browse files Browse the repository at this point in the history
  • Loading branch information
JWCook committed Dec 11, 2023
1 parent 4e6229d commit 575cdaa
Show file tree
Hide file tree
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
Expand Up @@ -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
Expand Up @@ -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
Expand Up @@ -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)
Expand Down
3 changes: 2 additions & 1 deletion docs/user_guide/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
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
Expand Down
2 changes: 1 addition & 1 deletion pyinaturalist/controllers/annotation_controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
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
Expand Down
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
Expand Down

0 comments on commit 575cdaa

Please sign in to comment.