diff --git a/docs/index.md b/docs/index.md index c884bc97..10246437 100644 --- a/docs/index.md +++ b/docs/index.md @@ -20,4 +20,4 @@ contributors :maxdepth: 1 history -``` \ No newline at end of file +``` diff --git a/docs/user_guide/advanced.md b/docs/user_guide/advanced.md index 83e75a43..b0fc1db3 100644 --- a/docs/user_guide/advanced.md +++ b/docs/user_guide/advanced.md @@ -366,4 +366,4 @@ session object used to make requests: ```python >>> from pyinaturalist import ClientSession >>> session = ClientSession(user_agent='my_app/1.0.0') -``` \ No newline at end of file +``` diff --git a/docs/user_guide/client.md b/docs/user_guide/client.md new file mode 100644 index 00000000..8fbb46e0 --- /dev/null +++ b/docs/user_guide/client.md @@ -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 ` 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 ` 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 `: +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. diff --git a/docs/user_guide/general.md b/docs/user_guide/general.md index 07ce7048..c9e0753b 100644 --- a/docs/user_guide/general.md +++ b/docs/user_guide/general.md @@ -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 `.from_json()` (single object) or `.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) diff --git a/docs/user_guide/index.md b/docs/user_guide/index.md index 87ebfa50..d14b611e 100644 --- a/docs/user_guide/index.md +++ b/docs/user_guide/index.md @@ -6,4 +6,5 @@ general advanced -``` \ No newline at end of file +client +``` diff --git a/pyinaturalist/client.py b/pyinaturalist/client.py index a8655203..4ae39126 100644 --- a/pyinaturalist/client.py +++ b/pyinaturalist/client.py @@ -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 ` instead of JSON. See :ref:`Controller classes ` 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 ` - >>> 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 diff --git a/pyinaturalist/controllers/__init__.py b/pyinaturalist/controllers/__init__.py index 630ae63a..5756f4a0 100644 --- a/pyinaturalist/controllers/__init__.py +++ b/pyinaturalist/controllers/__init__.py @@ -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 diff --git a/pyinaturalist/controllers/annotation_controller.py b/pyinaturalist/controllers/annotation_controller.py index 6f52478b..8ed6017a 100644 --- a/pyinaturalist/controllers/annotation_controller.py +++ b/pyinaturalist/controllers/annotation_controller.py @@ -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) diff --git a/pyinaturalist/models/__init__.py b/pyinaturalist/models/__init__.py index 3a8b8f33..0efb0758 100644 --- a/pyinaturalist/models/__init__.py +++ b/pyinaturalist/models/__init__.py @@ -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 diff --git a/pyinaturalist/paginator.py b/pyinaturalist/paginator.py index d75cbd5a..5abd7c72 100644 --- a/pyinaturalist/paginator.py +++ b/pyinaturalist/paginator.py @@ -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