Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API client class + initial draft of Observation controller #165

Merged
merged 6 commits into from
Jul 30, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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