diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index a5e73fd0..3a5f402c 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -20,5 +20,5 @@ _Is there an existing workaround for this issue?_ ### Environment - OS & version: [e.g. Debian 10] - - Python version: [e.g. `3.9`] - - Pyinaturalist version or branch: [e.g. `0.13` or `main` branch] + - Python version: [e.g. `3.12`] + - Pyinaturalist version or branch: [e.g. `0.19` or `main` branch] diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index 8c9fe4da..f319d9b4 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -1,7 +1,12 @@ --- name: "\U00002754 Question" -about: 'Question about how to use pyinaturalist' +about: 'Question about pyinaturalist' title: '' labels: question --- + + diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index d1c3376a..2495ae29 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,12 +1,17 @@ # New API Endpoint(s): Closes # + ### Checklist * **Endpoint implementations** - [ ] e.g., `GET /observations` * **Docs** - [ ] (Optional) add a response formatting function to `pyinaturalist.formatters` - - [ ] Add doctrings + type annotations to `pyinaturalist.api_docs` + - [ ] Add doctrings + type annotations - [ ] Add usage example(s) to API function docstring - [ ] Update release notes in `HISTORY.md` - [ ] Verify that docs build correctly in Sphinx diff --git a/HISTORY.md b/HISTORY.md index f0ed9791..c9362b94 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -4,6 +4,7 @@ ### New Endpoints * Added new **User** endpoint: `get_current_user()` +* Added new **Taxon** endpoint: `get_life_list_metadata()` ### Modified Endpoints Add support for searching observations by observation fields, using a new `observation_fields` param for the following functions: diff --git a/README.md b/README.md index 0b2c916d..ea0b4ef6 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # pyinaturalist -[![Build](https://github.com/pyinat/pyinaturalist/workflows/Build/badge.svg)](https://github.com/pyinat/pyinaturalist/actions) +[![Build](https://github.com/pyinat/pyinaturalist/workflows/Build/badge.svg?branch=main)](https://github.com/pyinat/pyinaturalist/actions) [![Codecov](https://codecov.io/gh/pyinat/pyinaturalist/branch/main/graph/badge.svg)](https://codecov.io/gh/pyinat/pyinaturalist) [![Documentation](https://img.shields.io/readthedocs/pyinaturalist/stable)](https://pyinaturalist.readthedocs.io) @@ -128,7 +128,7 @@ token = get_access_token( app_secret='my_app_secret', ) ``` -See [Authentication](https://pyinaturalist.readthedocs.io/en/latest/user_guide.html#authentication) +See [Authentication](https://pyinaturalist.readthedocs.io/en/stable/user_guide.html#authentication) for more options including environment variables, keyrings, and password managers. Now we can [create a new observation](https://pyinaturalist.readthedocs.io/en/stable/modules/pyinaturalist.v1.observations.html#pyinaturalist.v1.observations.create_observation): @@ -183,13 +183,13 @@ As with observations, there is a lot of information in the response, but we'll p ## Next Steps For more information, see: -* [User Guide](https://pyinaturalist.readthedocs.io/en/latest/user_guide.html): +* [User Guide](https://pyinaturalist.readthedocs.io/en/stable/user_guide.html): introduction and general features that apply to most endpoints -* [Endpoint Summary](https://pyinaturalist.readthedocs.io/en/latest/endpoints.html): +* [Endpoint Summary](https://pyinaturalist.readthedocs.io/en/stable/endpoints.html): a complete list of endpoints wrapped by pyinaturalist * [Examples](https://pyinaturalist.readthedocs.io/en/stable/examples.html): data visualizations and other examples of things to do with iNaturalist data -* [Reference](https://pyinaturalist.readthedocs.io/en/latest/reference.html): Detailed API documentation +* [Reference](https://pyinaturalist.readthedocs.io/en/stable/reference.html): Detailed API documentation * [Contributing Guide](https://pyinaturalist.readthedocs.io/en/stable/contributing.html): development details for anyone interested in contributing to pyinaturalist * [History](https://github.com/pyinat/pyinaturalist/blob/dev/HISTORY.md): @@ -197,9 +197,7 @@ For more information, see: * [Issues](https://github.com/pyinat/pyinaturalist/issues): planned & proposed features ## Feedback -If you have any problems, suggestions, or questions about pyinaturalist, please let us know! -Just [create an issue](https://github.com/pyinat/pyinaturalist/issues/new/choose). -Also, **PRs are welcome!** +If you have any problems, suggestions, or questions about pyinaturalist, you are welcome to [create an issue](https://github.com/pyinat/pyinaturalist/issues/new/choose) or [discussion](https://github.com/orgs/pyinat/discussions). Also, **PRs are welcome!** **Note:** pyinaturalist is developed by members of the iNaturalist community, and is not endorsed by iNaturalist.org or the California Academy of Sciences. If you have non-python-specific questions diff --git a/docs/_static/collapsible_container.css b/docs/_static/collapsible_container.css deleted file mode 100644 index 52bb284b..00000000 --- a/docs/_static/collapsible_container.css +++ /dev/null @@ -1,19 +0,0 @@ -/* Adapted from: https://github.com/plone/training/blob/master/_static/custom.css */ - -.toggle .admonition-title { - display: block; - clear: both; - cursor: pointer; -} - -.toggle .admonition-title:after { - content: " ▶"; -} - -.toggle .admonition-title.open:after { - content: " ▼"; -} - -.toggle p:last-child { - margin-bottom: 0; -} diff --git a/docs/_static/collapsible_container.js b/docs/_static/collapsible_container.js deleted file mode 100644 index 75042d68..00000000 --- a/docs/_static/collapsible_container.js +++ /dev/null @@ -1,10 +0,0 @@ -// Taken from: https://github.com/plone/training/blob/master/_templates/page.html - -$(document).ready(function() { - $(".toggle > *").hide(); - $(".toggle .admonition-title").show(); - $(".toggle .admonition-title").click(function() { - $(this).parent().children().not(".admonition-title").toggle(400); - $(this).parent().children(".admonition-title").toggleClass("open"); - }) -}); diff --git a/docs/_templates/module.rst_t b/docs/_templates/module.rst_t index fc80d804..0040a247 100644 --- a/docs/_templates/module.rst_t +++ b/docs/_templates/module.rst_t @@ -2,6 +2,16 @@ Summary ^^^^^^^ +{# + TODO: Include these sections only if classes/functions exist in the current module. + The automodule directive currently doesn't pass enough info into the template context + to make this possible. +#} +.. rubric:: Classes +.. automodsumm:: {{ qualname }} + :classes-only: + +.. rubric:: Functions .. automodsumm:: {{ qualname }} :functions-only: diff --git a/docs/conf.py b/docs/conf.py index ed271599..44037db9 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -66,6 +66,7 @@ 'sphinx_copybutton', 'sphinx_design', 'sphinxcontrib.apidoc', + 'sphinxext.opengraph', 'myst_parser', 'nbsphinx', ] @@ -92,7 +93,7 @@ # Enable automatic links to other projects' Sphinx docs intersphinx_mapping = { 'python': ('https://docs.python.org/3', None), - 'requests': ('https://requests.readthedocs.io/en/master/', None), + 'requests': ('https://requests.readthedocs.io/en/latest/', None), 'requests_cache': ('https://requests-cache.readthedocs.io/en/stable/', None), 'requests_ratelimiter': ('https://requests-ratelimiter.readthedocs.io/en/latest/', None), 'urllib3': ('https://urllib3.readthedocs.io/en/stable/', None), @@ -148,13 +149,17 @@ autosummary_imported_members = False numpydoc_show_class_members = False +# OpenGraph settings +ogp_site_url = 'https://pyinaturalist.readthedocs.io' +ogp_image = ( + 'https://raw.githubusercontent.com/pyinat/pyinaturalist/main/docs/images/python-logo-green.png' +) + # HTML general settings html_static_path = ['_static'] html_favicon = join('_static', 'favicon.ico') html_logo = join('_static', 'pyinaturalist_logo.png') -html_js_files = ['collapsible_container.js'] html_css_files = [ - 'collapsible_container.css', 'colors.css', 'table.css', 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.1.1/css/all.min.css', @@ -190,6 +195,7 @@ def setup(app): * https://docs.readthedocs.io/en/stable/builds.html * https://github.com/sphinx-contrib/apidoc """ + app.connect('config-inited', patch_sphinx_jinja_extensions) app.connect('builder-inited', document_models) app.connect('builder-inited', setup_external_files) app.connect('builder-inited', patch_automodapi) @@ -211,6 +217,21 @@ def make_symlink(src, dest): symlink(src, dest) +# TODO: Surely there's an easier way to do this? +def patch_sphinx_jinja_extensions(*args): + """Monkey-patch Sphinx to enable Jinja extensions""" + from jinja2 import Environment + from sphinx.jinja2glue import SphinxFileSystemLoader + + original_get_source = SphinxFileSystemLoader.get_source + + def get_source(self, environment: Environment, template: str): + environment.add_extension('jinja2.ext.debug') + return original_get_source(self, environment, template) + + SphinxFileSystemLoader.get_source = get_source + + def patch_automodapi(app): """Monkey-patch the automodapi extension to exclude imported members. diff --git a/docs/endpoints.md b/docs/endpoints.md index fe4ac0cf..b5630254 100644 --- a/docs/endpoints.md +++ b/docs/endpoints.md @@ -59,7 +59,7 @@ For all available endpoints, see: | GET | /taxa/{id} | {py:func}`.get_taxa_by_id` | | GET | /taxa/{id}/map_layers | {py:func}`.get_taxa_map_layers` | | GET | /taxa/autocomplete | {py:func}`.get_taxa_autocomplete` | -| GET | /taxa/lifelist_metadata | {py:func}`.get_lifelist_metadata` | +| GET | /taxa/lifelist_metadata | {py:func}`.get_life_list_metadata` | | GET | /users/{id} | {py:func}`.get_user_by_id` | | GET | /users/{id}/projects | | GET | /users/autocomplete | {py:func}`.get_users_autocomplete` | diff --git a/docs/examples.md b/docs/examples.md index 4df43555..1cdd9697 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -44,8 +44,7 @@ TODO: Can't generate thumbnails for Altair visualizations :end-line: 6 ``` -:::{admonition} Example code -:class: toggle +:::{dropdown} Example code ```{literalinclude} ../examples/observations_to_gpx.py :lines: 1,8- @@ -59,8 +58,7 @@ TODO: Can't generate thumbnails for Altair visualizations :end-line: 17 ``` -:::{admonition} Example code -:class: toggle +:::{dropdown} Example code ```{literalinclude} ../examples/observation_photo_metadata.py :lines: 1,19- diff --git a/docs/index.md b/docs/index.md index fae1e428..10246437 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,11 +9,15 @@ ```{toctree} :maxdepth: 2 -user_guide +user_guide/index examples endpoints reference contributing contributors +``` +```{toctree} +:maxdepth: 1 + history ``` diff --git a/docs/user_guide.md b/docs/user_guide/advanced.md similarity index 60% rename from docs/user_guide.md rename to docs/user_guide/advanced.md index 40042d3a..b0fc1db3 100644 --- a/docs/user_guide.md +++ b/docs/user_guide/advanced.md @@ -1,172 +1,5 @@ -(user_guide)= -# {fa}`book` User Guide -This page summarizes how to use the main features of pyinaturalist. - -## Installation -Installation instructions: - -::::{tab-set} -:::{tab-item} Pip -Install the latest stable version with pip: -``` -pip install pyinaturalist -``` -::: -:::{tab-item} Conda -Or install from conda-forge, if you prefer: -``` -conda install -c conda-forge pyinaturalist -``` -::: -:::{tab-item} Pre-release -If you would like to use the latest development (pre-release) version: -``` -pip install --pre pyinaturalist -``` -::: -:::{tab-item} Local development -See {ref}`contributing` for details on setup for local development. -::: -:::: - -:::{admonition} Python version compatibility -:class: toggle, tip - -pyinaturalist currently requires **python 3.7+**. If you need to use an older version -of python, here are the last compatible versions of pyinaturalist: - -* **python 2.7:** pyinaturalist 0.1 -* **python 3.4:** pyinaturalist 0.10 -* **python 3.5:** pyinaturalist 0.11 -* **python 3.6:** pyinaturalist 0.16 -* **python 3.7:** still supported, but expected to be dropped in a future release -::: - -## Imports -You can import all public functions and classes from the top-level `pyinaturalist` package: -``` ->>> from pyinaturalist import Taxon, get_observations, pprint # etc. -``` - -Or you can just import everything: -``` ->>> from pyinaturalist import * -``` - -## Requests -Requests generally follow the same format as the [API](https://api.inaturalist.org/v1) -and [search URLs](https://forum.inaturalist.org/t/how-to-use-inaturalists-search-urls-wiki). - -For example, if you wanted to search observations by user, these three requests are equivalent: - -::::{tab-set} -:::{tab-item} search URL -``` -https://www.inaturalist.org/observations?user_id=tiwane,jdmore -``` -::: -:::{tab-item} API request -``` -https://api.inaturalist.org/v1/observations?user_id=tiwane%2Cjdmore -``` -::: -:::{tab-item} pyinaturalist search -```python ->>> get_observations(user_id=['tiwane', 'jdmore']) -``` -::: -:::: - -Compared to search URLs and raw API requests, pyinaturalist provides some conveniences for making -requests easier: -* Python lists instead of comma-separated strings -* Python booleans instead of JS-style boolean strings or `1`/`0` -* Python file-like objects or file paths for photo and sound uploads -* Python {py:class}`~datetime.date` and {py:class}`~datetime.datetime` objects instead of date/time strings -* Simplified data formats for `POST` and `PUT` requests -* Simplified pagination -* Validation for multiple-choice parameters - -And more, depending on the function. -See the {ref}`reference-docs` section for a complete list of functions available. - -## Responses -API responses are returned as JSON, with some python type conversions applied (similar to the -request type conversions mentioned above). Example response data is shown in the documentation for -each request function, for example {py:func}`~pyinaturalist.v1.observations.get_observations`. - -### API Data vs Web UI -Here is how some of those response fields correspond to observation details shown on -iNaturalist.org: -```{figure} images/inat-observation-page-annotated.png -``` - -And here is what that same observation looks like in JSON: -:::{admonition} Observation response JSON -:class: toggle -```{literalinclude} sample_data/get_observation_2.json -``` -::: - -### Previewing Responses -These responses can contain large amounts of response attributes, making it somewhat cumbersome if you -just want to quickly preview results (for example, in a Jupyter notebook). For that purpose, the -{py:func}`~pyinaturalist.formatters.pprint` function is available to format response data as a -condensed, color-highlighted table. - -**Examples:** - -::::{tab-set} -:::{tab-item} Observations -``` ->>> from pyinaturalist import get_observations, pprint ->>> observations = get_observations(user_id='niconoe', per_page=5) ->>> pprint(observations) -ID Taxon ID Taxon Observed on User Location -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -82974075 61546 Species: Nemophora degeerella (Yellow-barred Longhorn) Jun 14, 2021 niconoe 1428 Braine-l'Alleud, Belgique -82827577 48201 Family: Scarabaeidae (Scarabs) Jun 13, 2021 niconoe 1428 Braine-l'Alleud, Belgique -82826778 48201 Family: Scarabaeidae (Scarabs) Jun 13, 2021 niconoe 1428 Braine-l'Alleud, Belgique -82696354 209660 Species: Chrysolina americana (Rosemary Beetle) Jun 12, 2021 niconoe 1420 Braine-l'Alleud, Belgique -82696334 472617 Species: Tomocerus vulgaris Jun 07, 2021 niconoe 1428 Braine-l'Alleud, Belgique -``` -::: -:::{tab-item} Places -``` ->>> from pyinaturalist import get_places, pprint ->>> places = get_places_autocomplete('Vale') ->>> pprint(places) - ID Latitude Longitude Name Category URL -━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ -96877 49.5189 -2.5190 Vale https://www.inaturalist.org/places/96877 -21951 -16.8960 -40.8349 Fronteira dos Vales https://www.inaturalist.org/places/21951 -23663 -6.3677 -41.8001 Valença do Piauí https://www.inaturalist.org/places/23663 -24222 -27.2220 -53.6338 Pinheirinho do Vale https://www.inaturalist.org/places/24222 -24374 -29.8309 -52.1121 Vale Verde https://www.inaturalist.org/places/24374 -24442 -10.3841 -62.0939 Vale do Paraíso https://www.inaturalist.org/places/24442 -103902 44.7355 27.5412 Valea Ciorii https://www.inaturalist.org/places/103902 -103905 44.7529 26.8481 Valea Macrisului https://www.inaturalist.org/places/103905 -105015 44.6805 24.0224 Valea Mare https://www.inaturalist.org/places/105015 -104268 46.7917 27.0905 Valea Ursului https://www.inaturalist.org/places/104268 -``` -::: -:::{tab-item} Places (with terminal colors) -```{figure} images/pprint_table.png -``` -::: -:::: - -## 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 ->>> 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. +# Advanced Usage +This page describes some advanced features of pyinaturalist. ## Pagination Most endpoints support pagination, using the parameters: @@ -197,14 +30,14 @@ you will need to use an access token. 3. Pass that access token to any API request function that uses it ### Creating an Application -:::{admonition} Why do I need to create an application? -:class: toggle, tip +:::{dropdown} Why do I need to create an application? +:icon: info iNaturalist uses OAuth2, which provides several different methods (or "flows") to access the site. For example, on the [login page](https://www.inaturalist.org/login), you have the option of logging in with a username/password, or with an external provider (Google, Facebook, etc.): -```{image} images/inat-user-login.png +```{image} ../images/inat-user-login.png :alt: Login form :width: 150 ``` @@ -227,14 +60,14 @@ following pieces of information: * **URL and Redirect URI:** Just enter the URL to your GitHub repo, if you have one; otherwise any placeholder like "" will work. -```{image} images/inat-new-application.png +```{image} ../images/inat-new-application.png :alt: New Application form :width: 300 ``` You should then see a screen like this, which will show your new application ID and secret. These will only be shown once, so save them somewhere secure, preferably in a password manager. -```{image} images/inat-new-application-complete.png +```{image} ../images/inat-new-application-complete.png :alt: Completed application form :width: 400 ``` @@ -347,7 +180,7 @@ across multiple machines. [KeePassXC](https://keepassxc.org/) offers this featur macOS and Linux systems. See this guide for setup info: [KeepassXC and secret service, a small walk-through](https://avaldes.co/2020/01/28/secret-service-keepassxc.html). -```{figure} images/password_manager_keying.png +```{figure} ../images/password_manager_keying.png Credentials storage with keyring + KeePassXC ``` @@ -359,7 +192,7 @@ See Caching and Rate-Limiting sections below for examples. ## Caching All API requests are cached by default. These expire in 30 minutes for most endpoints, and 1 day for some infrequently-changing data (like taxa and places). See -[requests-cache: Expiration](https://requests-cache.readthedocs.io/en/latest/user_guide/expiration.html) +[requests-cache: Expiration](https://requests-cache.readthedocs.io/en/stable/user_guide/expiration.html) for details on cache expiration behavior. You can change this behavior using {py:class}`.ClientSession`. For example, to keep cached requests for 5 days: @@ -534,7 +367,3 @@ session object used to make requests: >>> from pyinaturalist import ClientSession >>> session = ClientSession(user_agent='my_app/1.0.0') ``` - -## API Recommended Practices -See [API Recommended Practices](https://www.inaturalist.org/pages/api+recommended+practices) -on iNaturalist for more general usage information and notes. 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 new file mode 100644 index 00000000..c9e0753b --- /dev/null +++ b/docs/user_guide/general.md @@ -0,0 +1,184 @@ +# General Usage +This page summarizes how to use the main features of pyinaturalist. + +## Installation +Installation instructions: + +::::{tab-set} +:::{tab-item} Pip +Install the latest stable version with pip: +``` +pip install pyinaturalist +``` +::: +:::{tab-item} Conda +Or install from conda-forge, if you prefer: +``` +conda install -c conda-forge pyinaturalist +``` +::: +:::{tab-item} Pre-release +If you would like to use the latest development (pre-release) version: +``` +pip install --pre pyinaturalist +``` +::: +:::{tab-item} Local development +See {ref}`contributing` for details on setup for local development. +::: +:::: + +:::{dropdown} Python version compatibility +:icon: info + +pyinaturalist currently requires **python 3.7+**. If you need to use an older version +of python, here are the last compatible versions of pyinaturalist: + +* **python 2.7:** pyinaturalist 0.1 +* **python 3.4:** pyinaturalist 0.10 +* **python 3.5:** pyinaturalist 0.11 +* **python 3.6:** pyinaturalist 0.16 +* **python 3.7:** still supported, but expected to be dropped in a future release +::: + +## Imports +You can import all public functions and classes from the top-level `pyinaturalist` package: +``` +>>> from pyinaturalist import Taxon, get_observations, pprint # etc. +``` + +Or you can just import everything: +``` +>>> from pyinaturalist import * +``` + +## Requests +Requests generally follow the same format as the [API](https://api.inaturalist.org/v1) +and [search URLs](https://forum.inaturalist.org/t/how-to-use-inaturalists-search-urls-wiki). + +For example, if you wanted to search observations by user, these three requests are equivalent: + +::::{tab-set} +:::{tab-item} search URL +``` +https://www.inaturalist.org/observations?user_id=tiwane,jdmore +``` +::: +:::{tab-item} API request +``` +https://api.inaturalist.org/v1/observations?user_id=tiwane%2Cjdmore +``` +::: +:::{tab-item} pyinaturalist search +```python +>>> get_observations(user_id=['tiwane', 'jdmore']) +``` +::: +:::: + +Compared to search URLs and raw API requests, pyinaturalist provides some conveniences for making +requests easier: +* Python lists instead of comma-separated strings +* Python booleans instead of JS-style boolean strings or `1`/`0` +* Python file-like objects or file paths for photo and sound uploads +* Python {py:class}`~datetime.date` and {py:class}`~datetime.datetime` objects instead of date/time strings +* Simplified data formats for `POST` and `PUT` requests +* Simplified pagination +* Validation for multiple-choice parameters + +And more, depending on the function. +See the {ref}`reference-docs` section for a complete list of functions available. + +## Responses +API responses are returned as JSON, with some python type conversions applied (similar to the +request type conversions mentioned above). Example response data is shown in the documentation for +each request function, for example {py:func}`~pyinaturalist.v1.observations.get_observations`. + +### API Data vs Web UI +Here is how some of those response fields correspond to observation details shown on +iNaturalist.org: +```{figure} ../images/inat-observation-page-annotated.png +``` + +And here is what that same observation looks like in JSON: +:::{dropdown} Observation response JSON +:icon: code-square +```{literalinclude} ../sample_data/get_observation_2.json +``` +::: + +### Previewing Responses +These responses can contain large amounts of response attributes, making it somewhat cumbersome if you +just want to quickly preview results (for example, in a Jupyter notebook). For that purpose, the +{py:func}`~pyinaturalist.formatters.pprint` function is available to format response data as a +condensed, color-highlighted table. + +**Examples:** + +::::{tab-set} +:::{tab-item} Observations +``` +>>> from pyinaturalist import get_observations, pprint +>>> observations = get_observations(user_id='niconoe', per_page=5) +>>> pprint(observations) +ID Taxon ID Taxon Observed on User Location +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +82974075 61546 Species: Nemophora degeerella (Yellow-barred Longhorn) Jun 14, 2021 niconoe 1428 Braine-l'Alleud, Belgique +82827577 48201 Family: Scarabaeidae (Scarabs) Jun 13, 2021 niconoe 1428 Braine-l'Alleud, Belgique +82826778 48201 Family: Scarabaeidae (Scarabs) Jun 13, 2021 niconoe 1428 Braine-l'Alleud, Belgique +82696354 209660 Species: Chrysolina americana (Rosemary Beetle) Jun 12, 2021 niconoe 1420 Braine-l'Alleud, Belgique +82696334 472617 Species: Tomocerus vulgaris Jun 07, 2021 niconoe 1428 Braine-l'Alleud, Belgique +``` +::: +:::{tab-item} Places +``` +>>> from pyinaturalist import get_places, pprint +>>> places = get_places_autocomplete('Vale') +>>> pprint(places) + ID Latitude Longitude Name Category URL +━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ +96877 49.5189 -2.5190 Vale https://www.inaturalist.org/places/96877 +21951 -16.8960 -40.8349 Fronteira dos Vales https://www.inaturalist.org/places/21951 +23663 -6.3677 -41.8001 Valença do Piauí https://www.inaturalist.org/places/23663 +24222 -27.2220 -53.6338 Pinheirinho do Vale https://www.inaturalist.org/places/24222 +24374 -29.8309 -52.1121 Vale Verde https://www.inaturalist.org/places/24374 +24442 -10.3841 -62.0939 Vale do Paraíso https://www.inaturalist.org/places/24442 +103902 44.7355 27.5412 Valea Ciorii https://www.inaturalist.org/places/103902 +103905 44.7529 26.8481 Valea Macrisului https://www.inaturalist.org/places/103905 +105015 44.6805 24.0224 Valea Mare https://www.inaturalist.org/places/105015 +104268 46.7917 27.0905 Valea Ursului https://www.inaturalist.org/places/104268 +``` +::: +:::{tab-item} Places (with terminal colors) +```{figure} ../images/pprint_table.png +``` +::: +:::: + +(data-models)= +## Models +Data models ({py:mod}`pyinaturalist.models`) are included for all API response types. These allow +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) +``` + +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) +on iNaturalist for more general usage information and notes. diff --git a/docs/user_guide/index.md b/docs/user_guide/index.md new file mode 100644 index 00000000..d14b611e --- /dev/null +++ b/docs/user_guide/index.md @@ -0,0 +1,10 @@ +(user-guide)= +# {fa}`book` User Guide + +```{toctree} +:maxdepth: 2 + +general +advanced +client +``` diff --git a/examples/Tutorial_1_Observations.ipynb b/examples/Tutorial_1_Observations.ipynb index cf2f2cfb..895ea433 100644 --- a/examples/Tutorial_1_Observations.ipynb +++ b/examples/Tutorial_1_Observations.ipynb @@ -283,7 +283,7 @@ "Here is how some of those fields correspond to what you see on an observation page on iNaturalist.org:\n", "\n", "\n", - "You'll notice that there are many more fields available; see the [Observation docs](https://pyinaturalist.readthedocs.io/en/latest/modules/pyinaturalist.models.Observation.html) for a complete list." + "You'll notice that there are many more fields available; see the [Observation docs](https://pyinaturalist.readthedocs.io/en/stable/modules/pyinaturalist.models.Observation.html) for a complete list." ] }, { @@ -788,7 +788,7 @@ "metadata": {}, "source": [ "### Observation photo grid\n", - "We can use [ipyplot](https://github.com/karolzak/ipyplot) to preview observation images. `Observation.photos` contains a list of [Photo](https://pyinaturalist.readthedocs.io/en/latest/modules/pyinaturalist.models.Photo.html#pyinaturalist.models.Photo) objects, and we can use those to get a thumnail URL for first photo from each observation. For image labels, just call `str(observation)` to get a summary of the observation (who/what/when/where)." + "We can use [ipyplot](https://github.com/karolzak/ipyplot) to preview observation images. `Observation.photos` contains a list of [Photo](https://pyinaturalist.readthedocs.io/en/stable/modules/pyinaturalist.models.Photo.html#pyinaturalist.models.Photo) objects, and we can use those to get a thumnail URL for first photo from each observation. For image labels, just call `str(observation)` to get a summary of the observation (who/what/when/where)." ] }, { diff --git a/poetry.lock b/poetry.lock index f97cb8b6..6182efc0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -601,13 +601,13 @@ tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipyth [[package]] name = "fastjsonschema" -version = "2.18.1" +version = "2.19.0" description = "Fastest Python implementation of JSON schema" optional = true python-versions = "*" files = [ - {file = "fastjsonschema-2.18.1-py3-none-any.whl", hash = "sha256:aec6a19e9f66e9810ab371cc913ad5f4e9e479b63a7072a2cd060a9369e329a8"}, - {file = "fastjsonschema-2.18.1.tar.gz", hash = "sha256:06dc8680d937628e993fa0cd278f196d20449a1adc087640710846b324d422ea"}, + {file = "fastjsonschema-2.19.0-py3-none-any.whl", hash = "sha256:b9fd1a2dd6971dbc7fee280a95bd199ae0dd9ce22beb91cc75e9c1c528a5170e"}, + {file = "fastjsonschema-2.19.0.tar.gz", hash = "sha256:e25df6647e1bc4a26070b700897b07b542ec898dd4f1f6ea013e7f6a88417225"}, ] [package.extras] @@ -840,13 +840,13 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "jsonschema" -version = "4.19.2" +version = "4.20.0" description = "An implementation of JSON Schema validation for Python" optional = true python-versions = ">=3.8" files = [ - {file = "jsonschema-4.19.2-py3-none-any.whl", hash = "sha256:eee9e502c788e89cb166d4d37f43084e3b64ab405c795c03d343a4dbc2c810fc"}, - {file = "jsonschema-4.19.2.tar.gz", hash = "sha256:c9ff4d7447eed9592c23a12ccee508baf0dd0d59650615e847feb6cdca74f392"}, + {file = "jsonschema-4.20.0-py3-none-any.whl", hash = "sha256:ed6231f0429ecf966f5bc8dfef245998220549cbbcf140f913b7464c52c3b6b3"}, + {file = "jsonschema-4.20.0.tar.gz", hash = "sha256:4f614fd46d8d61258610998997743ec5492a648b33cf478c1ddc23ed4598a5fa"}, ] [package.dependencies] @@ -863,18 +863,18 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- [[package]] name = "jsonschema-specifications" -version = "2023.7.1" +version = "2023.11.1" description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" optional = true python-versions = ">=3.8" files = [ - {file = "jsonschema_specifications-2023.7.1-py3-none-any.whl", hash = "sha256:05adf340b659828a004220a9613be00fa3f223f2b82002e273dee62fd50524b1"}, - {file = "jsonschema_specifications-2023.7.1.tar.gz", hash = "sha256:c91a50404e88a1f6ba40636778e2ee08f6e24c5613fe4c53ac24578a5a7f72bb"}, + {file = "jsonschema_specifications-2023.11.1-py3-none-any.whl", hash = "sha256:f596778ab612b3fd29f72ea0d990393d0540a5aab18bf0407a46632eab540779"}, + {file = "jsonschema_specifications-2023.11.1.tar.gz", hash = "sha256:c9b234904ffe02f079bf91b14d79987faa685fd4b39c377a0996954c0090b9ca"}, ] [package.dependencies] importlib-resources = {version = ">=1.4.0", markers = "python_version < \"3.9\""} -referencing = ">=0.28.0" +referencing = ">=0.31.0" [[package]] name = "jupyter-client" @@ -1236,13 +1236,13 @@ testing-docutils = ["pygments", "pytest (>=7,<8)", "pytest-param-files (>=0.3.4, [[package]] name = "nbclient" -version = "0.8.0" +version = "0.9.0" description = "A client library for executing notebooks. Formerly nbconvert's ExecutePreprocessor." optional = true python-versions = ">=3.8.0" files = [ - {file = "nbclient-0.8.0-py3-none-any.whl", hash = "sha256:25e861299e5303a0477568557c4045eccc7a34c17fc08e7959558707b9ebe548"}, - {file = "nbclient-0.8.0.tar.gz", hash = "sha256:f9b179cd4b2d7bca965f900a2ebf0db4a12ebff2f36a711cb66861e4ae158e55"}, + {file = "nbclient-0.9.0-py3-none-any.whl", hash = "sha256:a3a1ddfb34d4a9d17fc744d655962714a866639acd30130e9be84191cd97cd15"}, + {file = "nbclient-0.9.0.tar.gz", hash = "sha256:4b28c207877cf33ef3a9838cdc7a54c5ceff981194a82eac59d558f05487295e"}, ] [package.dependencies] @@ -1425,13 +1425,13 @@ testing = ["docopt", "pytest (<6.0.0)"] [[package]] name = "pbr" -version = "5.11.1" +version = "6.0.0" description = "Python Build Reasonableness" optional = true python-versions = ">=2.6" files = [ - {file = "pbr-5.11.1-py2.py3-none-any.whl", hash = "sha256:567f09558bae2b3ab53cb3c1e2e33e726ff3338e7bae3db5dc954b3a44eef12b"}, - {file = "pbr-5.11.1.tar.gz", hash = "sha256:aefc51675b0b533d56bb5fd1c8c6c0522fe31896679882e1c4c63d5e4a0fccb3"}, + {file = "pbr-6.0.0-py2.py3-none-any.whl", hash = "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda"}, + {file = "pbr-6.0.0.tar.gz", hash = "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9"}, ] [[package]] @@ -1541,13 +1541,13 @@ colorama = "*" [[package]] name = "prompt-toolkit" -version = "3.0.39" +version = "3.0.41" description = "Library for building powerful interactive command lines in Python" optional = true python-versions = ">=3.7.0" files = [ - {file = "prompt_toolkit-3.0.39-py3-none-any.whl", hash = "sha256:9dffbe1d8acf91e3de75f3b544e4842382fc06c6babe903ac9acb74dc6e08d88"}, - {file = "prompt_toolkit-3.0.39.tar.gz", hash = "sha256:04505ade687dc26dc4284b1ad19a83be2f2afe83e7a828ace0c72f3a1df72aac"}, + {file = "prompt_toolkit-3.0.41-py3-none-any.whl", hash = "sha256:f36fe301fafb7470e86aaf90f036eef600a3210be4decf461a5b1ca8403d3cb2"}, + {file = "prompt_toolkit-3.0.41.tar.gz", hash = "sha256:941367d97fc815548822aa26c2a269fdc4eb21e9ec05fc5d447cf09bad5d75f0"}, ] [package.dependencies] @@ -1680,13 +1680,13 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale [[package]] name = "pytest-xdist" -version = "3.3.1" +version = "3.4.0" description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" optional = false python-versions = ">=3.7" files = [ - {file = "pytest-xdist-3.3.1.tar.gz", hash = "sha256:d5ee0520eb1b7bcca50a60a518ab7a7707992812c578198f8b44fdfac78e8c93"}, - {file = "pytest_xdist-3.3.1-py3-none-any.whl", hash = "sha256:ff9daa7793569e6a68544850fd3927cd257cc03a7ef76c95e86915355e82b5f2"}, + {file = "pytest-xdist-3.4.0.tar.gz", hash = "sha256:3a94a931dd9e268e0b871a877d09fe2efb6175c2c23d60d56a6001359002b832"}, + {file = "pytest_xdist-3.4.0-py3-none-any.whl", hash = "sha256:e513118bf787677a427e025606f55e95937565e06dfaac8d87f55301e57ae607"}, ] [package.dependencies] @@ -1928,13 +1928,13 @@ cffi = {version = "*", markers = "implementation_name == \"pypy\""} [[package]] name = "referencing" -version = "0.30.2" +version = "0.31.0" description = "JSON Referencing + Python" optional = true python-versions = ">=3.8" files = [ - {file = "referencing-0.30.2-py3-none-any.whl", hash = "sha256:449b6669b6121a9e96a7f9e410b245d471e8d48964c67113ce9afe50c8dd7bdf"}, - {file = "referencing-0.30.2.tar.gz", hash = "sha256:794ad8003c65938edcdbc027f1933215e0d0ccc0291e3ce20a4d87432b59efc0"}, + {file = "referencing-0.31.0-py3-none-any.whl", hash = "sha256:381b11e53dd93babb55696c71cf42aef2d36b8a150c49bf0bc301e36d536c882"}, + {file = "referencing-0.31.0.tar.gz", hash = "sha256:cc28f2c88fbe7b961a7817a0abc034c09a1e36358f82fedb4ffdf29a25398863"}, ] [package.dependencies] @@ -2031,13 +2031,13 @@ docs = ["furo (>=2022.12,<2023.0)", "myst-parser (>=0.17)", "sphinx (>=5.2,<6.0) [[package]] name = "rich" -version = "13.6.0" +version = "13.7.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.6.0-py3-none-any.whl", hash = "sha256:2b38e2fe9ca72c9a00170a1a2d20c63c790d0e10ef1fe35eba76e1e7b1d7d245"}, - {file = "rich-13.6.0.tar.gz", hash = "sha256:5c14d22737e6d5084ef4771b62d5d4363165b403455a30a1c8ca39dc7b644bef"}, + {file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"}, + {file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"}, ] [package.dependencies] @@ -2050,110 +2050,110 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "rpds-py" -version = "0.12.0" +version = "0.13.0" description = "Python bindings to Rust's persistent data structures (rpds)" optional = true python-versions = ">=3.8" files = [ - {file = "rpds_py-0.12.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:c694bee70ece3b232df4678448fdda245fd3b1bb4ba481fb6cd20e13bb784c46"}, - {file = "rpds_py-0.12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:30e5ce9f501fb1f970e4a59098028cf20676dee64fc496d55c33e04bbbee097d"}, - {file = "rpds_py-0.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d72a4315514e5a0b9837a086cb433b004eea630afb0cc129de76d77654a9606f"}, - {file = "rpds_py-0.12.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eebaf8c76c39604d52852366249ab807fe6f7a3ffb0dd5484b9944917244cdbe"}, - {file = "rpds_py-0.12.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a239303acb0315091d54c7ff36712dba24554993b9a93941cf301391d8a997ee"}, - {file = "rpds_py-0.12.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ced40cdbb6dd47a032725a038896cceae9ce267d340f59508b23537f05455431"}, - {file = "rpds_py-0.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c8c0226c71bd0ce9892eaf6afa77ae8f43a3d9313124a03df0b389c01f832de"}, - {file = "rpds_py-0.12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b8e11715178f3608874508f08e990d3771e0b8c66c73eb4e183038d600a9b274"}, - {file = "rpds_py-0.12.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5210a0018c7e09c75fa788648617ebba861ae242944111d3079034e14498223f"}, - {file = "rpds_py-0.12.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:171d9a159f1b2f42a42a64a985e4ba46fc7268c78299272ceba970743a67ee50"}, - {file = "rpds_py-0.12.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:57ec6baec231bb19bb5fd5fc7bae21231860a1605174b11585660236627e390e"}, - {file = "rpds_py-0.12.0-cp310-none-win32.whl", hash = "sha256:7188ddc1a8887194f984fa4110d5a3d5b9b5cd35f6bafdff1b649049cbc0ce29"}, - {file = "rpds_py-0.12.0-cp310-none-win_amd64.whl", hash = "sha256:1e04581c6117ad9479b6cfae313e212fe0dfa226ac727755f0d539cd54792963"}, - {file = "rpds_py-0.12.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:0a38612d07a36138507d69646c470aedbfe2b75b43a4643f7bd8e51e52779624"}, - {file = "rpds_py-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f12d69d568f5647ec503b64932874dade5a20255736c89936bf690951a5e79f5"}, - {file = "rpds_py-0.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8a1d990dc198a6c68ec3d9a637ba1ce489b38cbfb65440a27901afbc5df575"}, - {file = "rpds_py-0.12.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8c567c664fc2f44130a20edac73e0a867f8e012bf7370276f15c6adc3586c37c"}, - {file = "rpds_py-0.12.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0e9e976e0dbed4f51c56db10831c9623d0fd67aac02853fe5476262e5a22acb7"}, - {file = "rpds_py-0.12.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:efddca2d02254a52078c35cadad34762adbae3ff01c6b0c7787b59d038b63e0d"}, - {file = "rpds_py-0.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d9e7f29c00577aff6b318681e730a519b235af292732a149337f6aaa4d1c5e31"}, - {file = "rpds_py-0.12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:389c0e38358fdc4e38e9995e7291269a3aead7acfcf8942010ee7bc5baee091c"}, - {file = "rpds_py-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:33ab498f9ac30598b6406e2be1b45fd231195b83d948ebd4bd77f337cb6a2bff"}, - {file = "rpds_py-0.12.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:d56b1cd606ba4cedd64bb43479d56580e147c6ef3f5d1c5e64203a1adab784a2"}, - {file = "rpds_py-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1fa73ed22c40a1bec98d7c93b5659cd35abcfa5a0a95ce876b91adbda170537c"}, - {file = "rpds_py-0.12.0-cp311-none-win32.whl", hash = "sha256:dbc25baa6abb205766fb8606f8263b02c3503a55957fcb4576a6bb0a59d37d10"}, - {file = "rpds_py-0.12.0-cp311-none-win_amd64.whl", hash = "sha256:c6b52b7028b547866c2413f614ee306c2d4eafdd444b1ff656bf3295bf1484aa"}, - {file = "rpds_py-0.12.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:9620650c364c01ed5b497dcae7c3d4b948daeae6e1883ae185fef1c927b6b534"}, - {file = "rpds_py-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2124f9e645a94ab7c853bc0a3644e0ca8ffbe5bb2d72db49aef8f9ec1c285733"}, - {file = "rpds_py-0.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:281c8b219d4f4b3581b918b816764098d04964915b2f272d1476654143801aa2"}, - {file = "rpds_py-0.12.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:27ccc93c7457ef890b0dd31564d2a05e1aca330623c942b7e818e9e7c2669ee4"}, - {file = "rpds_py-0.12.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1c562a9bb72244fa767d1c1ab55ca1d92dd5f7c4d77878fee5483a22ffac808"}, - {file = "rpds_py-0.12.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e57919c32ee295a2fca458bb73e4b20b05c115627f96f95a10f9f5acbd61172d"}, - {file = "rpds_py-0.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa35ad36440aaf1ac8332b4a4a433d4acd28f1613f0d480995f5cfd3580e90b7"}, - {file = "rpds_py-0.12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e6aea5c0eb5b0faf52c7b5c4a47c8bb64437173be97227c819ffa31801fa4e34"}, - {file = "rpds_py-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:81cf9d306c04df1b45971c13167dc3bad625808aa01281d55f3cf852dde0e206"}, - {file = "rpds_py-0.12.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:08e6e7ff286254016b945e1ab632ee843e43d45e40683b66dd12b73791366dd1"}, - {file = "rpds_py-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4d0a675a7acbbc16179188d8c6d0afb8628604fc1241faf41007255957335a0b"}, - {file = "rpds_py-0.12.0-cp312-none-win32.whl", hash = "sha256:b2287c09482949e0ca0c0eb68b2aca6cf57f8af8c6dfd29dcd3bc45f17b57978"}, - {file = "rpds_py-0.12.0-cp312-none-win_amd64.whl", hash = "sha256:8015835494b21aa7abd3b43fdea0614ee35ef6b03db7ecba9beb58eadf01c24f"}, - {file = "rpds_py-0.12.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:6174d6ad6b58a6bcf67afbbf1723420a53d06c4b89f4c50763d6fa0a6ac9afd2"}, - {file = "rpds_py-0.12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a689e1ded7137552bea36305a7a16ad2b40be511740b80748d3140614993db98"}, - {file = "rpds_py-0.12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f45321224144c25a62052035ce96cbcf264667bcb0d81823b1bbc22c4addd194"}, - {file = "rpds_py-0.12.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa32205358a76bf578854bf31698a86dc8b2cb591fd1d79a833283f4a403f04b"}, - {file = "rpds_py-0.12.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91bd2b7cf0f4d252eec8b7046fa6a43cee17e8acdfc00eaa8b3dbf2f9a59d061"}, - {file = "rpds_py-0.12.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3acadbab8b59f63b87b518e09c4c64b142e7286b9ca7a208107d6f9f4c393c5c"}, - {file = "rpds_py-0.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:429349a510da82c85431f0f3e66212d83efe9fd2850f50f339341b6532c62fe4"}, - {file = "rpds_py-0.12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05942656cb2cb4989cd50ced52df16be94d344eae5097e8583966a1d27da73a5"}, - {file = "rpds_py-0.12.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0c5441b7626c29dbd54a3f6f3713ec8e956b009f419ffdaaa3c80eaf98ddb523"}, - {file = "rpds_py-0.12.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:b6b0e17d39d21698185097652c611f9cf30f7c56ccec189789920e3e7f1cee56"}, - {file = "rpds_py-0.12.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3b7a64d43e2a1fa2dd46b678e00cabd9a49ebb123b339ce799204c44a593ae1c"}, - {file = "rpds_py-0.12.0-cp38-none-win32.whl", hash = "sha256:e5bbe011a2cea9060fef1bb3d668a2fd8432b8888e6d92e74c9c794d3c101595"}, - {file = "rpds_py-0.12.0-cp38-none-win_amd64.whl", hash = "sha256:bec29b801b4adbf388314c0d050e851d53762ab424af22657021ce4b6eb41543"}, - {file = "rpds_py-0.12.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:1096ca0bf2d3426cbe79d4ccc91dc5aaa73629b08ea2d8467375fad8447ce11a"}, - {file = "rpds_py-0.12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48aa98987d54a46e13e6954880056c204700c65616af4395d1f0639eba11764b"}, - {file = "rpds_py-0.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7979d90ee2190d000129598c2b0c82f13053dba432b94e45e68253b09bb1f0f6"}, - {file = "rpds_py-0.12.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:88857060b690a57d2ea8569bca58758143c8faa4639fb17d745ce60ff84c867e"}, - {file = "rpds_py-0.12.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4eb74d44776b0fb0782560ea84d986dffec8ddd94947f383eba2284b0f32e35e"}, - {file = "rpds_py-0.12.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f62581d7e884dd01ee1707b7c21148f61f2febb7de092ae2f108743fcbef5985"}, - {file = "rpds_py-0.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f5dcb658d597410bb7c967c1d24eaf9377b0d621358cbe9d2ff804e5dd12e81"}, - {file = "rpds_py-0.12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bf9acce44e967a5103fcd820fc7580c7b0ab8583eec4e2051aec560f7b31a63"}, - {file = "rpds_py-0.12.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:240687b5be0f91fbde4936a329c9b7589d9259742766f74de575e1b2046575e4"}, - {file = "rpds_py-0.12.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:25740fb56e8bd37692ed380e15ec734be44d7c71974d8993f452b4527814601e"}, - {file = "rpds_py-0.12.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a54917b7e9cd3a67e429a630e237a90b096e0ba18897bfb99ee8bd1068a5fea0"}, - {file = "rpds_py-0.12.0-cp39-none-win32.whl", hash = "sha256:b92aafcfab3d41580d54aca35a8057341f1cfc7c9af9e8bdfc652f83a20ced31"}, - {file = "rpds_py-0.12.0-cp39-none-win_amd64.whl", hash = "sha256:cd316dbcc74c76266ba94eb021b0cc090b97cca122f50bd7a845f587ff4bf03f"}, - {file = "rpds_py-0.12.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:0853da3d5e9bc6a07b2486054a410b7b03f34046c123c6561b535bb48cc509e1"}, - {file = "rpds_py-0.12.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:cb41ad20064e18a900dd427d7cf41cfaec83bcd1184001f3d91a1f76b3fcea4e"}, - {file = "rpds_py-0.12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b710bf7e7ae61957d5c4026b486be593ed3ec3dca3e5be15e0f6d8cf5d0a4990"}, - {file = "rpds_py-0.12.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a952ae3eb460c6712388ac2ec706d24b0e651b9396d90c9a9e0a69eb27737fdc"}, - {file = "rpds_py-0.12.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0bedd91ae1dd142a4dc15970ed2c729ff6c73f33a40fa84ed0cdbf55de87c777"}, - {file = "rpds_py-0.12.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:761531076df51309075133a6bc1db02d98ec7f66e22b064b1d513bc909f29743"}, - {file = "rpds_py-0.12.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2baa6be130e8a00b6cbb9f18a33611ec150b4537f8563bddadb54c1b74b8193"}, - {file = "rpds_py-0.12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f05450fa1cd7c525c0b9d1a7916e595d3041ac0afbed2ff6926e5afb6a781b7f"}, - {file = "rpds_py-0.12.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:81c4d1a3a564775c44732b94135d06e33417e829ff25226c164664f4a1046213"}, - {file = "rpds_py-0.12.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:e888be685fa42d8b8a3d3911d5604d14db87538aa7d0b29b1a7ea80d354c732d"}, - {file = "rpds_py-0.12.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:6f8d7fe73d1816eeb5378409adc658f9525ecbfaf9e1ede1e2d67a338b0c7348"}, - {file = "rpds_py-0.12.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:0831d3ecdea22e4559cc1793f22e77067c9d8c451d55ae6a75bf1d116a8e7f42"}, - {file = "rpds_py-0.12.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:513ccbf7420c30e283c25c82d5a8f439d625a838d3ba69e79a110c260c46813f"}, - {file = "rpds_py-0.12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:301bd744a1adaa2f6a5e06c98f1ac2b6f8dc31a5c23b838f862d65e32fca0d4b"}, - {file = "rpds_py-0.12.0-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8832a4f83d4782a8f5a7b831c47e8ffe164e43c2c148c8160ed9a6d630bc02a"}, - {file = "rpds_py-0.12.0-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b2416ed743ec5debcf61e1242e012652a4348de14ecc7df3512da072b074440"}, - {file = "rpds_py-0.12.0-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35585a8cb5917161f42c2104567bb83a1d96194095fc54a543113ed5df9fa436"}, - {file = "rpds_py-0.12.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d389ff1e95b6e46ebedccf7fd1fadd10559add595ac6a7c2ea730268325f832c"}, - {file = "rpds_py-0.12.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9b007c2444705a2dc4a525964fd4dd28c3320b19b3410da6517cab28716f27d3"}, - {file = "rpds_py-0.12.0-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:188912b22b6c8225f4c4ffa020a2baa6ad8fabb3c141a12dbe6edbb34e7f1425"}, - {file = "rpds_py-0.12.0-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:1b4cf9ab9a0ae0cb122685209806d3f1dcb63b9fccdf1424fb42a129dc8c2faa"}, - {file = "rpds_py-0.12.0-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:2d34a5450a402b00d20aeb7632489ffa2556ca7b26f4a63c35f6fccae1977427"}, - {file = "rpds_py-0.12.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:466030a42724780794dea71eb32db83cc51214d66ab3fb3156edd88b9c8f0d78"}, - {file = "rpds_py-0.12.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:68172622a5a57deb079a2c78511c40f91193548e8ab342c31e8cb0764d362459"}, - {file = "rpds_py-0.12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54cdfcda59251b9c2f87a05d038c2ae02121219a04d4a1e6fc345794295bdc07"}, - {file = "rpds_py-0.12.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6b75b912a0baa033350367a8a07a8b2d44fd5b90c890bfbd063a8a5f945f644b"}, - {file = "rpds_py-0.12.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47aeceb4363851d17f63069318ba5721ae695d9da55d599b4d6fb31508595278"}, - {file = "rpds_py-0.12.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0525847f83f506aa1e28eb2057b696fe38217e12931c8b1b02198cfe6975e142"}, - {file = "rpds_py-0.12.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efbe0b5e0fd078ed7b005faa0170da4f72666360f66f0bb2d7f73526ecfd99f9"}, - {file = "rpds_py-0.12.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0fadfdda275c838cba5102c7f90a20f2abd7727bf8f4a2b654a5b617529c5c18"}, - {file = "rpds_py-0.12.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:56dd500411d03c5e9927a1eb55621e906837a83b02350a9dc401247d0353717c"}, - {file = "rpds_py-0.12.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:6915fc9fa6b3ec3569566832e1bb03bd801c12cea030200e68663b9a87974e76"}, - {file = "rpds_py-0.12.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5f1519b080d8ce0a814f17ad9fb49fb3a1d4d7ce5891f5c85fc38631ca3a8dc4"}, - {file = "rpds_py-0.12.0.tar.gz", hash = "sha256:7036316cc26b93e401cedd781a579be606dad174829e6ad9e9c5a0da6e036f80"}, + {file = "rpds_py-0.13.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:1758197cc8d7ff383c07405f188253535b4aa7fa745cbc54d221ae84b18e0702"}, + {file = "rpds_py-0.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:715df74cbcef4387d623c917f295352127f4b3e0388038d68fa577b4e4c6e540"}, + {file = "rpds_py-0.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8a9cec0f49df9bac252d92f138c0d7708d98828e21fd57db78087d8f50b5656"}, + {file = "rpds_py-0.13.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5c2545bba02f68abdf398ef4990dc77592cc1e5d29438b35b3a3ca34d171fb4b"}, + {file = "rpds_py-0.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:95375c44ffb9ea2bc25d67fb66e726ea266ff1572df50b9556fe28a5f3519cd7"}, + {file = "rpds_py-0.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54e513df45a8a9419e7952ffd26ac9a5b7b1df97fe72530421794b0de29f9d72"}, + {file = "rpds_py-0.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a25f514a53927b6b4bd04a9a6a13b55209df54f548660eeed673336c0c946d14"}, + {file = "rpds_py-0.13.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1a920fa679ec2758411d66bf68840b0a21317b9954ab0e973742d723bb67709"}, + {file = "rpds_py-0.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f9339d1404b87e6d8cb35e485945753be57a99ab9bb389f42629215b2f6bda0f"}, + {file = "rpds_py-0.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c99f9dda2c959f7bb69a7125e192c74fcafb7a534a95ccf49313ae3a04807804"}, + {file = "rpds_py-0.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bad6758df5f1042b35683bd1811d5432ac1b17700a5a2a51fdc293f7df5f7827"}, + {file = "rpds_py-0.13.0-cp310-none-win32.whl", hash = "sha256:2a29ec68fa9655ce9501bc6ae074b166e8b45c2dfcd2d71d90d1a61758ed8c73"}, + {file = "rpds_py-0.13.0-cp310-none-win_amd64.whl", hash = "sha256:244be953f13f148b0071d67a610f89cd72eb5013a147e517d6ca3f3f3b7e0380"}, + {file = "rpds_py-0.13.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:240279ca0b2afd6d4710afce1c94bf9e75fc161290bf62c0feba64d64780d80b"}, + {file = "rpds_py-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:25c9727da2dabc93664a18eda7a70feedf478f0c4c8294e4cdba7f60a479a246"}, + {file = "rpds_py-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:981e46e1e5064f95460381bff4353783b4b5ce351c930e5b507ebe0278c61dac"}, + {file = "rpds_py-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6052bb47ea583646b8ff562acacb9a2ec5ec847267049cbae3919671929e94c6"}, + {file = "rpds_py-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:87f591ff8cc834fa01ca5899ab5edcd7ee590492a9cdcf43424ac142e731ce3e"}, + {file = "rpds_py-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62772259b3381e2aabf274c74fd1e1ac03b0524de0a6593900684becfa8cfe4b"}, + {file = "rpds_py-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4de9d20fe68c16b4d97f551a09920745add0c86430262230528b83c2ed2fe90"}, + {file = "rpds_py-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b70a54fb628c1d6400e351674a31ba63d2912b8c5b707f99b408674a5d8b69ab"}, + {file = "rpds_py-0.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2063ab9cd1be7ef6b5ed0f408e2bdf32c060b6f40c097a468f32864731302636"}, + {file = "rpds_py-0.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:84f7f3f18d29a1c645729634003d21d84028bd9c2fd78eba9d028998f46fa5aa"}, + {file = "rpds_py-0.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7c7ddc8d1a64623068da5a15e28001fbd0f0aff754aae7a75a4be5042191638"}, + {file = "rpds_py-0.13.0-cp311-none-win32.whl", hash = "sha256:8a33d2b6340261191bb59adb5a453fa6c7d99de85552bd4e8196411f0509c9bf"}, + {file = "rpds_py-0.13.0-cp311-none-win_amd64.whl", hash = "sha256:8b9c1dd90461940315981499df62a627571c4f0992e8bafc5396d33916224cac"}, + {file = "rpds_py-0.13.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:15a2d542de5cbfc6abddc4846d9412b59f8ee9c8dfa0b9c92a29321297c91745"}, + {file = "rpds_py-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8dd69e01b29ff45a0062cad5c480d8aa9301c3ef09da471f86337a78eb2d3405"}, + {file = "rpds_py-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efdd02971a02f98492a72b25484f1f6125fb9f2166e48cc4c9bfa563349c851b"}, + {file = "rpds_py-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:91ca9aaee7ccdfa66d800b5c4ec634fefca947721bab52d6ad2f6350969a3771"}, + {file = "rpds_py-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:afcec1f5b09d0db70aeb2d90528a9164acb61841a3124e28f6ac0137f4c36cb4"}, + {file = "rpds_py-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c6824673f66c47f7ee759c21e973bfce3ceaf2c25cb940cb45b41105dc914e8"}, + {file = "rpds_py-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50b6d80925dfeb573fc5e38582fb9517c6912dc462cc858a11c8177b0837127a"}, + {file = "rpds_py-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a1a38512925829784b5dc38591c757b80cfce115c72c594dc59567dab62b9c4"}, + {file = "rpds_py-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:977c6123c359dcc70ce3161b781ab70b0d342de2666944b776617e01a0a7822a"}, + {file = "rpds_py-0.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c472409037e05ed87b99430f97a6b82130328bb977502813547e8ee6a3392502"}, + {file = "rpds_py-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:28bb22019f4a783ea06a6b81437d5996551869e8a722ee8720b744f7684d97f4"}, + {file = "rpds_py-0.13.0-cp312-none-win32.whl", hash = "sha256:46be9c0685cce2ea02151aa8308f2c1b78581be41a5dd239448a941a210ef5dd"}, + {file = "rpds_py-0.13.0-cp312-none-win_amd64.whl", hash = "sha256:3c5b9ad4d3e05dfcf8629f0d534f92610e9805dbce2fcb9b3c801ddb886431d5"}, + {file = "rpds_py-0.13.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:66eb5aa36e857f768c598d2082fafb733eaf53e06e1169c6b4de65636e04ffd0"}, + {file = "rpds_py-0.13.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c9f4c2b7d989426e9fe9b720211172cf10eb5f7aa16c63de2e5dc61457abcf35"}, + {file = "rpds_py-0.13.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e37dfffe8959a492b7b331995f291847a41a035b4aad82d6060f38e8378a2b"}, + {file = "rpds_py-0.13.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8220321f2dccd9d66f72639185247cb7bbdd90753bf0b6bfca0fa31dba8af23c"}, + {file = "rpds_py-0.13.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8f1d466a9747213d3cf7e1afec849cc51edb70d5b4ae9a82eca0f172bfbb6d0"}, + {file = "rpds_py-0.13.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c4c4b4ff3de834ec5c1c690e5a18233ca78547d003eb83664668ccf09ef1398"}, + {file = "rpds_py-0.13.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:525d19ef0a999229ef0f0a7687ab2c9a00d1b6a47a005006f4d8c4b8975fdcec"}, + {file = "rpds_py-0.13.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0982b59d014efb84a57128e7e69399fb29ad8f2da5b0a5bcbfd12e211c00492e"}, + {file = "rpds_py-0.13.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f714dd5b705f1c394d1b361d96486c4981055c434a7eafb1a3147ac75e34a3de"}, + {file = "rpds_py-0.13.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:766b573a964389ef0d91a26bb31e1b59dbc5d06eff7707f3dfcec23d93080ba3"}, + {file = "rpds_py-0.13.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:2ed65ad3fc5065d13e31e90794e0b52e405b63ae4fab1080caeaadc10a3439c5"}, + {file = "rpds_py-0.13.0-cp38-none-win32.whl", hash = "sha256:9645f7fe10a68b2396d238250b4b264c2632d2eb6ce2cb90aa0fe08adee194be"}, + {file = "rpds_py-0.13.0-cp38-none-win_amd64.whl", hash = "sha256:42d0ad129c102856a364ccc7d356faec017af86b3543a8539795f22b6cabad11"}, + {file = "rpds_py-0.13.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:95c11647fac2a3515ea2614a79e14b7c75025724ad54c91c7db4a6ea5c25ef19"}, + {file = "rpds_py-0.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9435bf4832555c4f769c6be9401664357be33d5f5d8dc58f5c20fb8d21e2c45d"}, + {file = "rpds_py-0.13.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54b1d671a74395344239ee3adbcd8c496525f6a2b2e54c40fec69620a31a8dcb"}, + {file = "rpds_py-0.13.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13c8061115f1468de6ffdfb1d31b446e1bd814f1ff6e556862169aacb9fbbc5d"}, + {file = "rpds_py-0.13.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a78861123b002725633871a2096c3a4313224aab3d11b953dced87cfba702418"}, + {file = "rpds_py-0.13.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97c1be5a018cdad54fa7e5f7d36b9ab45ef941a1d185987f18bdab0a42344012"}, + {file = "rpds_py-0.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e33b17915c8e4fb2ea8b91bb4c46cba92242c63dd38b87e869ead5ba217e2970"}, + {file = "rpds_py-0.13.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:153b6d8cf7ae4b9ffd09de6abeda661e351e3e06eaafd18a8c104ea00099b131"}, + {file = "rpds_py-0.13.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:da2852201e8e00c86be82c43d6893e6c380ef648ae53f337ffd1eaa35e3dfb8a"}, + {file = "rpds_py-0.13.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a2383f400691fd7bd63347d4d75eb2fd525de9d901799a33a4e896c9885609f8"}, + {file = "rpds_py-0.13.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d5bf560634ea6e9a59ceb2181a6cd6195a03f48cef9a400eb15e197e18f14548"}, + {file = "rpds_py-0.13.0-cp39-none-win32.whl", hash = "sha256:fdaef49055cc0c701fb17b9b34a38ef375e5cdb230b3722d4a12baf9b7cbc6d3"}, + {file = "rpds_py-0.13.0-cp39-none-win_amd64.whl", hash = "sha256:26660c74a20fe249fad75ca00bbfcf60e57c3fdbde92971c88a20e07fea1de64"}, + {file = "rpds_py-0.13.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:28324f2f0247d407daabf7ff357ad9f36126075c92a0cf5319396d96ff4e1248"}, + {file = "rpds_py-0.13.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b431c2c0ff1ea56048a2b066d99d0c2d151ae7625b20be159b7e699f3e80390b"}, + {file = "rpds_py-0.13.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7472bd60a8293217444bdc6a46e516feb8d168da44d5f3fccea0336e88e3b79a"}, + {file = "rpds_py-0.13.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:169063f346b8fd84f47d986c9c48e6094eb38b839c1287e7cb886b8a2b32195d"}, + {file = "rpds_py-0.13.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eef7ee7c70f8b8698be468d54f9f5e01804f3a1dd5657e8a96363dbd52b9b5ec"}, + {file = "rpds_py-0.13.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:762013dd59df12380c5444f61ccbf9ae1297027cabbd7aa25891f724ebf8c8f7"}, + {file = "rpds_py-0.13.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:152570689a27ae0be1d5f50b21dad38d450b9227d0974f23bd400400ea087e88"}, + {file = "rpds_py-0.13.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d70a93a40e55da117c511ddc514642bc7d59a95a99137168a5f3f2f876b47962"}, + {file = "rpds_py-0.13.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e6c6fed07d13b9e0fb689356c40c81f1aa92e3c9d91d8fd5816a0348ccd999f7"}, + {file = "rpds_py-0.13.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:cdded3cf9e36840b09ccef714d5fa74a03f4eb6cf81e694226ed9cb5e6f90de0"}, + {file = "rpds_py-0.13.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:e1f40faf406c52c7ae7d208b9140377c06397248978ccb03fbfbb30a0571e359"}, + {file = "rpds_py-0.13.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:c10326e30c97a95b7e1d75e5200ef0b9827aa0f861e331e43b15dfdfd63e669b"}, + {file = "rpds_py-0.13.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:afde37e3763c602d0385bce5c12f262e7b1dd2a0f323e239fa9d7b2d4d5d8509"}, + {file = "rpds_py-0.13.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4084ab6840bd4d79eff3b5f497add847a7db31ce5a0c2d440c90b2d2b7011857"}, + {file = "rpds_py-0.13.0-pp38-pypy38_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1c9c9cb48ab77ebfa47db25b753f594d4f44959cfe43b713439ca6e3c9329671"}, + {file = "rpds_py-0.13.0-pp38-pypy38_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:533d728ea5ad5253af3395102723ca8a77b62de47b2295155650c9a88fcdeec8"}, + {file = "rpds_py-0.13.0-pp38-pypy38_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f22cab655b41033d430f20266bf563b35038a7f01c9a099b0ccfd30a7fb9247"}, + {file = "rpds_py-0.13.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a0507342c37132813449393e6e6f351bbff376031cfff1ee6e616402ac7908"}, + {file = "rpds_py-0.13.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4eb1faf8e2ee9a2de3cb3ae4c8c355914cdc85f2cd7f27edf76444c9550ce1e7"}, + {file = "rpds_py-0.13.0-pp38-pypy38_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:a61a152d61e3ae26e0bbba7b2f568f6f25ca0abdeb6553eca7e7c45b59d9b1a9"}, + {file = "rpds_py-0.13.0-pp38-pypy38_pp73-musllinux_1_2_i686.whl", hash = "sha256:e499bf2200eb74774a6f85a7465e3bc5273fa8ef0055590d97a88c1e7ea02eea"}, + {file = "rpds_py-0.13.0-pp38-pypy38_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:1e5becd0de924616ca9a12abeb6458568d1dc8fe5c670d5cdb738402a8a8429d"}, + {file = "rpds_py-0.13.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:70cfe098d915f566eeebcb683f49f9404d2f948432891b6e075354336eda9dfb"}, + {file = "rpds_py-0.13.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:2e73511e88368f93c24efe7c9a20b319eaa828bc7431f8a17713efb9e31a39fa"}, + {file = "rpds_py-0.13.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c07cb9bcccd08f9bc2fd05bf586479df4272ea5a6a70fbcb59b018ed48a5a84d"}, + {file = "rpds_py-0.13.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8c4e84016ba225e09df20fed8befe8c68d14fbeff6078f4a0ff907ae2095e17e"}, + {file = "rpds_py-0.13.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ad465e5a70580ca9c1944f43a9a71bca3a7b74554347fc96ca0479eca8981f9"}, + {file = "rpds_py-0.13.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:189aebd44a07fa7b7966cf78b85bde8335b0b6c3b1c4ef5589f8c03176830107"}, + {file = "rpds_py-0.13.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f50ca0460f1f7a89ab9b8355d83ac993d5998ad4218e76654ecf8afe648d8aa"}, + {file = "rpds_py-0.13.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f6c225011467021879c0482316e42d8a28852fc29f0c15d2a435ff457cadccd4"}, + {file = "rpds_py-0.13.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:1e63b32b856c0f08a56b76967d61b6ad811d8d330a8aebb9d21afadd82a296f6"}, + {file = "rpds_py-0.13.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:7e5fbe9800f09c56967fda88c4d9272955e781699a66102bd098f22511a3f260"}, + {file = "rpds_py-0.13.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:fea99967d4a978ce95dd52310bcb4a943b77c61725393bca631b0908047d6e2f"}, + {file = "rpds_py-0.13.0.tar.gz", hash = "sha256:35cc91cbb0b775705e0feb3362490b8418c408e9e3c3b9cb3b02f6e495f03ee7"}, ] [[package]] @@ -2472,6 +2472,20 @@ files = [ lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] +[[package]] +name = "sphinxext-opengraph" +version = "0.9.0" +description = "Sphinx Extension to enable OGP support" +optional = true +python-versions = ">=3.8" +files = [ + {file = "sphinxext-opengraph-0.9.0.tar.gz", hash = "sha256:4e57e25b6d56f47b9c06a5a5d68a2a00ed3577c8a39e459b52118c6bfe5e8c8b"}, + {file = "sphinxext_opengraph-0.9.0-py3-none-any.whl", hash = "sha256:ab1eb2ffb531fb85b695e719dba7b0245b0643f6b6c0d1cc258d15a81e72a9f1"}, +] + +[package.dependencies] +sphinx = ">=4.0" + [[package]] name = "stack-data" version = "0.6.3" @@ -2522,13 +2536,13 @@ files = [ [[package]] name = "tomlkit" -version = "0.12.2" +version = "0.12.3" description = "Style preserving TOML library" optional = false python-versions = ">=3.7" files = [ - {file = "tomlkit-0.12.2-py3-none-any.whl", hash = "sha256:eeea7ac7563faeab0a1ed8fe12c2e5a51c61f933f2502f7e9db0241a65163ad0"}, - {file = "tomlkit-0.12.2.tar.gz", hash = "sha256:df32fab589a81f0d7dc525a4267b6d7a64ee99619cbd1eeb0fae32c1dd426977"}, + {file = "tomlkit-0.12.3-py3-none-any.whl", hash = "sha256:b0a645a9156dc7cb5d3a1f0d4bab66db287fcb8e0430bdd4664a095ea16414ba"}, + {file = "tomlkit-0.12.3.tar.gz", hash = "sha256:75baf5012d06501f07bee5bf8e801b9f343e7aac5a92581f20f80ce632e6b5a4"}, ] [[package]] @@ -2719,13 +2733,13 @@ test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess [[package]] name = "wcwidth" -version = "0.2.9" +version = "0.2.10" description = "Measures the displayed width of unicode strings in a terminal" optional = true python-versions = "*" files = [ - {file = "wcwidth-0.2.9-py2.py3-none-any.whl", hash = "sha256:9a929bd8380f6cd9571a968a9c8f4353ca58d7cd812a4822bba831f8d685b223"}, - {file = "wcwidth-0.2.9.tar.gz", hash = "sha256:a675d1a4a2d24ef67096a04b85b02deeecd8e226f57b5e3a72dbb9ed99d27da8"}, + {file = "wcwidth-0.2.10-py2.py3-none-any.whl", hash = "sha256:aec5179002dd0f0d40c456026e74a729661c9d468e1ed64405e3a6c2176ca36f"}, + {file = "wcwidth-0.2.10.tar.gz", hash = "sha256:390c7454101092a6a5e43baad8f83de615463af459201709556b6e4b1c861f97"}, ] [[package]] @@ -2756,9 +2770,9 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [extras] all = ["ujson"] -docs = ["furo", "ipython", "linkify-it-py", "myst-parser", "nbsphinx", "sphinx", "sphinx-autodoc-typehints", "sphinx-automodapi", "sphinx-copybutton", "sphinx-design", "sphinxcontrib-apidoc"] +docs = ["furo", "ipython", "linkify-it-py", "myst-parser", "nbsphinx", "sphinx", "sphinx-autodoc-typehints", "sphinx-automodapi", "sphinx-copybutton", "sphinx-design", "sphinxcontrib-apidoc", "sphinxext-opengraph"] [metadata] lock-version = "2.0" python-versions = "^3.7" -content-hash = "5f88f53f2b7b5246b5b74f1361e4b29f0feb98ec6755d8bc32d39e495dbb56be" +content-hash = "e611f5b8dffdf68611e6343494a1a0ec2a1c68f873346c8beb5ee0d29c3d5ab0" 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 diff --git a/pyinaturalist/session.py b/pyinaturalist/session.py index faca4e52..001b8e1d 100644 --- a/pyinaturalist/session.py +++ b/pyinaturalist/session.py @@ -99,7 +99,7 @@ def __init__( cache_control: Use server-provided Cache-Control headers to set cache expiration when possible (instead of ``expire_after`` or ``urls_expire_after``) expire_after: How long to keep cached API requests; for advanced options, see - `requests-cache: Expiration `_ + `requests-cache: Expiration `_ urls_expire_after Glob patterns for per-URL cache expiration; See `requests-cache: URL Patterns `_ per_second: Max requests per second diff --git a/pyinaturalist/v0/observation_fields.py b/pyinaturalist/v0/observation_fields.py index 9ac53f2b..c84f34d8 100644 --- a/pyinaturalist/v0/observation_fields.py +++ b/pyinaturalist/v0/observation_fields.py @@ -24,8 +24,9 @@ def get_observation_fields(**params) -> JsonResponse: >>> from pprint import pprint >>> pprint({r['id']: r['name'] for r in response}) - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_observation_fields.py @@ -75,8 +76,9 @@ def put_observation_field_values( ... access_token=token, ... ) - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/post_put_observation_field_value.json :language: javascript diff --git a/pyinaturalist/v0/observations.py b/pyinaturalist/v0/observations.py index 14ab22b3..4208ca1c 100644 --- a/pyinaturalist/v0/observations.py +++ b/pyinaturalist/v0/observations.py @@ -35,37 +35,43 @@ def get_observations(**params) -> Union[List, str]: >>> get_observations(id=45414404, converters='atom') - .. admonition:: Example Response (atom) - :class: toggle + .. dropdown:: Example Response (atom) + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_observations.atom :language: xml - .. admonition:: Example Response (csv) - :class: toggle + .. dropdown:: Example Response (csv) + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_observations.csv - .. admonition:: Example Response (dwc) - :class: toggle + .. dropdown:: Example Response (dwc) + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_observations.dwc :language: xml - .. admonition:: Example Response (json) - :class: toggle + .. dropdown:: Example Response (json) + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_observations.json :language: json - .. admonition:: Example Response (kml) - :class: toggle + .. dropdown:: Example Response (kml) + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_observations.kml :language: xml - .. admonition:: Example Response (widget) - :class: toggle + .. dropdown:: Example Response (widget) + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_observations.js :language: javascript @@ -106,14 +112,16 @@ def create_observation(**params) -> ListResponse: >>> observation_fields={297: 1}, # 297 is the obs. field ID for 'Number of individuals' >>> ) - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/create_observation_result.json :language: javascript - .. admonition:: Example Response (failure) - :class: toggle + .. dropdown:: Example Response (failure) + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/create_observation_fail.json :language: javascript @@ -171,8 +179,9 @@ def update_observation(observation_id: int, **params) -> ListResponse: >>> description='updated description!', >>> ) - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/update_observation_result.json :language: javascript @@ -218,8 +227,9 @@ def upload_photos(observation_id: int, photos: MultiFile, **params) -> ListRespo >>> access_token=token, >>> ) - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/post_observation_photos_list.json :language: javascript @@ -261,8 +271,9 @@ def upload_sounds(observation_id: int, sounds: MultiFile, **params) -> ListRespo >>> token = get_access_token() >>> upload_sounds(1234, '~/observations/2020_09_01_14003156.mp3', access_token=token) - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/post_observation_sounds_list.json :language: javascript diff --git a/pyinaturalist/v1/controlled_terms.py b/pyinaturalist/v1/controlled_terms.py index 779b2967..84d4636f 100644 --- a/pyinaturalist/v1/controlled_terms.py +++ b/pyinaturalist/v1/controlled_terms.py @@ -21,8 +21,9 @@ def get_controlled_terms(taxon_id: Optional[int] = None, **params) -> JsonRespon 4: Pupa ... - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_controlled_terms.json :language: JSON @@ -53,8 +54,9 @@ def get_controlled_terms_for_taxon(taxon_id: int, **params) -> JsonResponse: 4: Pupa ... - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_controlled_terms_for_taxon.json :language: JSON diff --git a/pyinaturalist/v1/identifications.py b/pyinaturalist/v1/identifications.py index 84c2d14c..fc9bee35 100644 --- a/pyinaturalist/v1/identifications.py +++ b/pyinaturalist/v1/identifications.py @@ -17,8 +17,9 @@ def get_identifications_by_id(identification_id: MultiInt, **params) -> JsonResp Example: >>> get_identifications_by_id(155554373) - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_identifications.py @@ -52,8 +53,9 @@ def get_identifications(**params) -> JsonResponse: [147500725] Species: 1163860 (improving) added on 2020-12-24 23:52:30+00:00 by jkcook ... - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_identifications.py diff --git a/pyinaturalist/v1/messages.py b/pyinaturalist/v1/messages.py index b6b0d724..ed2e17df 100644 --- a/pyinaturalist/v1/messages.py +++ b/pyinaturalist/v1/messages.py @@ -22,8 +22,9 @@ def get_message_by_id(message_id: MultiInt, **params) -> JsonResponse: >>> response = get_messages(123456) >>> pprint(response) - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_messages.json @@ -49,8 +50,9 @@ def get_messages(**params) -> JsonResponse: >>> response = get_messages() >>> pprint(response) - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_messages.json diff --git a/pyinaturalist/v1/observation_fields.py b/pyinaturalist/v1/observation_fields.py index 2a3f0691..2db9ee21 100644 --- a/pyinaturalist/v1/observation_fields.py +++ b/pyinaturalist/v1/observation_fields.py @@ -31,8 +31,9 @@ def set_observation_field( ... access_token=token, ... ) - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/post_put_observation_field_value.json :language: javascript diff --git a/pyinaturalist/v1/observations.py b/pyinaturalist/v1/observations.py index af93c29d..d6322ba9 100644 --- a/pyinaturalist/v1/observations.py +++ b/pyinaturalist/v1/observations.py @@ -72,9 +72,9 @@ def get_observations(**params) -> JsonResponse: >>> response = get_observations(observation_fields={'Species count': 2}) - - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_observations_node.py @@ -111,8 +111,9 @@ def get_observations_by_id( >>> response = get_observations_by_id(16227955) >>> response = get_observations_by_id([16227955, 16227956]) - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_observations_by_id.py @@ -160,18 +161,21 @@ def get_observation_histogram(**params) -> HistogramResponse: >>> place_id=8057, >>> ) - .. admonition:: Example Response (observations per month of year) - :class: toggle + .. dropdown:: Example Response (observations per month of year) + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_observation_histogram_month_of_year.py - .. admonition:: Example Response (observations per month) - :class: toggle + .. dropdown:: Example Response (observations per month) + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_observation_histogram_month.py - .. admonition:: Example Response (observations per day) - :class: toggle + .. dropdown:: Example Response (observations per day) + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_observation_histogram_day.py @@ -200,8 +204,9 @@ def get_observation_identifiers(**params) -> JsonResponse: [691216 ] jbrown252 (James Brown) [3959037 ] tnsparkleberry - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_observation_identifiers_ex_results.json :language: JSON @@ -234,8 +239,9 @@ def get_observation_observers(**params) -> JsonResponse: [5813 ] fluffberger (Fluff Berger) - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_observation_observers_ex_results.json :language: JSON @@ -266,8 +272,9 @@ def get_observation_species_counts(**params) -> JsonResponse: [55727] Species: Cymbalaria muralis (Ivy-leaved toadflax): 3 ... - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_observation_species_counts.py @@ -294,8 +301,9 @@ def get_observation_popular_field_values(**params) -> JsonResponse: ... species_name='Danaus plexippus', place_id=24, ... ) - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_observation_popular_field_values.py @@ -322,8 +330,9 @@ def get_observation_taxonomy(**params) -> JsonResponse: Example: >>> response = get_observation_taxonomy(user_id='my_username') - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_observation_taxonomy.json :language: JSON @@ -348,8 +357,9 @@ def get_observation_taxon_summary(observation_id: int, **params) -> JsonResponse Example: >>> response = get_observation_taxon_summary(7849808) - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_observation_taxon_summary.py @@ -388,8 +398,9 @@ def create_observation(**params) -> JsonResponse: ... description='Updated description!', ... ) - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/create_observation_v1.json :language: JSON @@ -428,8 +439,9 @@ def update_observation(observation_id: int, **params) -> ListResponse: >>> description='updated description!', >>> ) - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/update_observation_result.json :language: javascript @@ -483,8 +495,9 @@ def upload( ... access_token=token, ... ) - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/upload_photos_and_sounds.json :language: JSON diff --git a/pyinaturalist/v1/places.py b/pyinaturalist/v1/places.py index a146722a..84434fb1 100644 --- a/pyinaturalist/v1/places.py +++ b/pyinaturalist/v1/places.py @@ -22,8 +22,9 @@ def get_places_by_id(place_id: MultiInt, **params) -> JsonResponse: [89191] Conservation Area Riversdale [67591] Riversdale Beach - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_places_by_id.py @@ -70,8 +71,9 @@ def get_places_nearby( [97393] Oceania ... - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_places_nearby.py @@ -103,8 +105,9 @@ def get_places_autocomplete(q: Optional[str] = None, **params) -> JsonResponse: [166186] Irkutsk Oblast - ADD [163077] Irkutsk agglomeration - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_places_autocomplete.py diff --git a/pyinaturalist/v1/projects.py b/pyinaturalist/v1/projects.py index c9e3047c..0d252541 100644 --- a/pyinaturalist/v1/projects.py +++ b/pyinaturalist/v1/projects.py @@ -46,8 +46,9 @@ def get_projects(**params) -> JsonResponse: [102925 ] Keechelus/Kachess Invasive Plants ... - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_projects.py @@ -83,8 +84,9 @@ def get_projects_by_id( [8348] Tucson High Native and Invasive Species Inventory [6432] CBWN Invasive Plants - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_projects_by_id.py @@ -129,8 +131,9 @@ def add_project_observation( Example: >>> add_project_observation(24237, 1234, access_token) - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/add_project_observation.json :language: JSON diff --git a/pyinaturalist/v1/search.py b/pyinaturalist/v1/search.py index 7f3ae8c6..f05641a5 100644 --- a/pyinaturalist/v1/search.py +++ b/pyinaturalist/v1/search.py @@ -21,8 +21,9 @@ def search(q: str, **params) -> JsonResponse: [Project] [9978 ] Ohio Dragonfly Survey (Ohio Odonata Survey) [User ] [113886 ] odonatanb (Gilles Belliveau) - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_search.py diff --git a/pyinaturalist/v1/taxa.py b/pyinaturalist/v1/taxa.py index a88ce366..d6a03108 100644 --- a/pyinaturalist/v1/taxa.py +++ b/pyinaturalist/v1/taxa.py @@ -25,8 +25,9 @@ def get_taxa(**params) -> JsonResponse: [646195] Genus: Vespiodes ... - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_taxa.json :language: JSON @@ -68,8 +69,9 @@ def get_taxa_by_id( 'wikipedia_summary': 'The Polistinae are eusocial wasps closely related to yellow jackets...', } - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_taxa_by_id.py @@ -128,13 +130,15 @@ def get_taxa_autocomplete(**params) -> JsonResponse: >>> first_result["matched_term"] "Zygocactus truncatus" # ...Because it matched an older synonym for Schlumbergera - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_taxa_autocomplete.py - .. admonition:: Example Response (formatted) - :class: toggle + .. dropdown:: Example Response (formatted) + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_taxa_autocomplete_minified.py @@ -154,8 +158,9 @@ def get_taxa_map_layers(taxon_id: int, **params) -> JsonResponse: Example: >>> response = get_taxa_map_layers(343248) - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_taxa_map_layers.json :language: JSON @@ -185,8 +190,9 @@ def get_life_list_metadata( Example: >>> response = get_life_list_metadata(user_id='my_username') - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_lifelist_metadata.json :language: JSON diff --git a/pyinaturalist/v1/users.py b/pyinaturalist/v1/users.py index b2592a45..995b262a 100644 --- a/pyinaturalist/v1/users.py +++ b/pyinaturalist/v1/users.py @@ -25,8 +25,9 @@ def get_user_by_id(user_id: IntOrStr, **params) -> JsonResponse: >>> pprint(response) [1234] my_username - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_user_by_id.py @@ -55,8 +56,9 @@ def get_users_autocomplete(q: str, **params) -> JsonResponse: [1234] my_username [12345] my_username_2 - .. admonition:: Example Response - :class: toggle + .. dropdown:: Example Response + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_users_autocomplete.py diff --git a/pyinaturalist/v2/observations.py b/pyinaturalist/v2/observations.py index 7e682b88..95ea069d 100644 --- a/pyinaturalist/v2/observations.py +++ b/pyinaturalist/v2/observations.py @@ -66,13 +66,15 @@ def get_observations(**params) -> JsonResponse: >>> response = get_observations(observation_fields={'Species count': 2}) - .. admonition:: Example Response (default/minimal) - :class: toggle + .. dropdown:: Example Response (default/minimal) + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_observations_v2_minimal.py - .. admonition:: Example Response (all fields) - :class: toggle + .. dropdown:: Example Response (all fields) + :color: primary + :icon: code-square .. literalinclude:: ../sample_data/get_observations_v2_full.py diff --git a/pyproject.toml b/pyproject.toml index e901543e..fbaefb33 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,6 +63,7 @@ sphinx-autodoc-typehints = {optional=true, python=">=3.8", version="1.20"} sphinx-copybutton = {optional=true, python=">=3.8", version=">=0.5"} sphinx-design = {optional=true, python=">=3.8", version=">=0.5"} sphinxcontrib-apidoc = {optional=true, python=">=3.8", version="^0.3"} +sphinxext-opengraph = {optional=true, python=">=3.8", version=">=0.9"} [tool.poetry.dev-dependencies] coverage = ">=7.2" @@ -79,8 +80,9 @@ sphinx-autobuild = {python=">=3.8", version=">=2021.3"} [tool.poetry.extras] all = ["ujson"] -docs = ["furo", "ipython", "linkify-it-py", "myst-parser", "nbsphinx", "sphinx", "sphinx-automodapi", - "sphinx-autodoc-typehints", "sphinx-copybutton", "sphinx-design", "sphinxcontrib-apidoc"] +docs = ["furo", "ipython", "linkify-it-py", "myst-parser", "nbsphinx", "sphinx", + "sphinx-automodapi", "sphinx-autodoc-typehints", "sphinx-copybutton", "sphinx-design", + "sphinxcontrib-apidoc", "sphinxext-opengraph"] [build-system] requires = ["poetry-core>=1.0.0"]