-
Notifications
You must be signed in to change notification settings - Fork 16
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #165 from JWCook/api-client
API client class + initial draft of Observation controller
- Loading branch information
Showing
5 changed files
with
115 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
# flake8: noqa: F401 | ||
from pyinaturalist.controllers.base import BaseController | ||
from pyinaturalist.controllers.observations import ObservationController |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
class BaseController: | ||
"""Base class for resource-specific controllers""" | ||
|
||
def __init__(self, client): | ||
self.client = client |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |