diff --git a/pyinaturalist/__init__.py b/pyinaturalist/__init__.py index 8c154449..f8bdffbf 100644 --- a/pyinaturalist/__init__.py +++ b/pyinaturalist/__init__.py @@ -6,6 +6,7 @@ # Ignore ImportErrors if this is imported outside a virtualenv try: from pyinaturalist.auth import get_access_token + from pyinaturalist.client import iNatClient from pyinaturalist.constants import * from pyinaturalist.formatters import enable_logging, format_table, pprint from pyinaturalist.models import * diff --git a/pyinaturalist/client.py b/pyinaturalist/client.py new file mode 100644 index 00000000..9ff58e6b --- /dev/null +++ b/pyinaturalist/client.py @@ -0,0 +1,52 @@ +# TODO: Use requests_cache.CachedSession by default? +# TODO: Create and refresh access tokens on demand (if using keyring)? +from logging import getLogger + +from pyrate_limiter import Limiter +from requests import Session + +from pyinaturalist import DEFAULT_USER_AGENT +from pyinaturalist.api_requests import RATE_LIMITER +from pyinaturalist.controllers import ObservationController + +logger = getLogger(__name__) + + +class iNatClient: + """API Client class. + 'iNatClient' is nonstandard casing, but 'InatClient' just looks wrong. Deal with it, pep8. + + Args: + dry_run: Just log all requests instead of sending real requests + limiter: Rate-limiting settings to use instead of the default + session: Session object to use instead of creating a new one + user_agent: User-Agent string to pass to API requests + """ + + def __init__( + self, + dry_run: bool = False, + limiter: Limiter = RATE_LIMITER, + session: Session = None, + user_agent: str = DEFAULT_USER_AGENT, + ): + self.access_token = None + self.dry_run = dry_run + self.limiter = limiter + self.session = session or Session() + self.user_agent = user_agent + + # Controllers + self.observations = ObservationController(self) + # self.taxa = TaxonController(self) + # etc. + + @property + def settings(self): + """Get client settings to pass to an API request""" + return { + 'dry_run': self.dry_run, + 'limiter': self.limiter, + 'session': self.session, + 'user_agent': self.user_agent, + } diff --git a/pyinaturalist/controllers/__init__.py b/pyinaturalist/controllers/__init__.py new file mode 100644 index 00000000..1f66214c --- /dev/null +++ b/pyinaturalist/controllers/__init__.py @@ -0,0 +1,3 @@ +# flake8: noqa: F401 +from pyinaturalist.controllers.base import BaseController +from pyinaturalist.controllers.observations import ObservationController diff --git a/pyinaturalist/controllers/base.py b/pyinaturalist/controllers/base.py new file mode 100644 index 00000000..315321a3 --- /dev/null +++ b/pyinaturalist/controllers/base.py @@ -0,0 +1,5 @@ +class BaseController: + """Base class for resource-specific controllers""" + + def __init__(self, client): + self.client = client diff --git a/pyinaturalist/controllers/observations.py b/pyinaturalist/controllers/observations.py new file mode 100644 index 00000000..848cb21b --- /dev/null +++ b/pyinaturalist/controllers/observations.py @@ -0,0 +1,54 @@ +# TODO: Update examples and example responses +# TODO: Don't override return signature +from typing import Dict, List + +from pyinaturalist.constants import HistogramResponse +from pyinaturalist.controllers import BaseController +from pyinaturalist.docs import copy_doc_signature +from pyinaturalist.models import LifeList, Observation, Taxon, User +from pyinaturalist.models.taxon import TaxonCounts +from pyinaturalist.v1 import ( + get_observation_histogram, + get_observation_identifiers, + get_observation_observers, + get_observation_species_counts, + get_observation_taxonomy, + get_observations, +) + + +# TODO: Fix type checking for return types +class ObservationController(BaseController): + """Controller for observation requests""" + + @copy_doc_signature(get_observations) + def search(self, **params) -> List[Observation]: + response = get_observations(**params, **self.client.settings) + return Observation.from_json_list(response) # type: ignore + + # TODO: Does this need a model with utility functions, or is {datetime: count} sufficient? + @copy_doc_signature(get_observation_histogram) + def histogram(self, **params) -> HistogramResponse: + return get_observation_histogram(**params, **self.client.settings) + + @copy_doc_signature(get_observation_identifiers) + def identifiers(self, **params) -> Dict[int, User]: + response = get_observation_identifiers(**params, **self.client.settings) + return {r['count']: User.from_json(r['user']) for r in response['results']} # type: ignore + + @copy_doc_signature(get_observation_taxonomy, add_common_args=False) + def life_list(self, *args, **params) -> LifeList: + response = get_observation_taxonomy(*args, **params, **self.client.settings) + return LifeList.from_json(response.json()) # type: ignore + + # TODO: Separate model for these results? (maybe a User subclass) + # TODO: Include species_counts + @copy_doc_signature(get_observation_observers) + def observers(self, **params) -> Dict[int, User]: + response = get_observation_observers(**params, **self.client.settings) + return {r['count']: User.from_json(r['user']) for r in response['results']} # type: ignore + + @copy_doc_signature(get_observation_species_counts) + def species_counts(self, **params) -> Dict[int, Taxon]: + response = get_observation_species_counts(**params, **self.client.settings) + return TaxonCounts.from_json(response) # type: ignore