Skip to content

Commit

Permalink
Merge pull request #165 from JWCook/api-client
Browse files Browse the repository at this point in the history
API client class + initial draft of Observation controller
  • Loading branch information
JWCook authored Jul 30, 2021
2 parents 13f514d + e9c986d commit d853a39
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 0 deletions.
1 change: 1 addition & 0 deletions pyinaturalist/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
Expand Down
52 changes: 52 additions & 0 deletions pyinaturalist/client.py
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,
}
3 changes: 3 additions & 0 deletions pyinaturalist/controllers/__init__.py
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
5 changes: 5 additions & 0 deletions pyinaturalist/controllers/base.py
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
54 changes: 54 additions & 0 deletions pyinaturalist/controllers/observations.py
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

0 comments on commit d853a39

Please sign in to comment.