Skip to content

Commit

Permalink
Add user controller
Browse files Browse the repository at this point in the history
  • Loading branch information
JWCook committed Sep 14, 2021
1 parent 6f3ff18 commit 78f383f
Show file tree
Hide file tree
Showing 11 changed files with 85 additions and 13 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ aims to make these data easily accessible in the python programming language.
- [Related Projects](#related-projects)

## Features
* **Easier requests:** Complete type annotations for request parameters, and simplified create/update request formats
* **Convenient responses:** Type conversions to the things you would expect in python
* **Easier requests:** Complete type annotations for request parameters, simplified create/update
request formats, and easy pagination
* **Convenient responses:** Type conversions to the things you would expect in python, and optional
models for an object-oriented inteface to response data
* **Docs:** Example requests, responses, scripts, and Jupyter notebooks to help get you started
* **Security:** Keyring integration for secure credential storage
* **Server-friendly:** Client-side rate-limiting that follows the
Expand Down
8 changes: 3 additions & 5 deletions docs/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,10 @@
# {fa}`list` API Reference
iNaturalist currently provides three API versions:

* The original [Rails-based API](https://www.inaturalist.org/pages/api+reference) (v0)
that they also use internally: it is very complete and provides read/write access, but has less
consistent responses. It is likely that this API will be deprecated at some point in the future.
* The original [Rails-based API](https://www.inaturalist.org/pages/api+reference) (v0). It is likely
that this API will be deprecated at some point in the future.
* The [Node-based API](https://api.inaturalist.org/v1/docs/) (v1) allows read and write access
to most iNaturalist resources, and is generally faster and more consistent. It is
missing some of the features of the v0 API, however.
to most iNaturalist resources, and is generally faster and more consistent than the v0 API.
* The [v2 API](https://api.inaturalist.org/v1/docs/) is currently in development, and will
eventually replace both the v0 and v1 APIs. see
[this forum thread](https://forum.inaturalist.org/t/obs-detail-on-api-v2-feedback/21215)
Expand Down
8 changes: 7 additions & 1 deletion pyinaturalist/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@

from pyinaturalist.auth import get_access_token
from pyinaturalist.constants import TOKEN_EXPIRATION, JsonResponse
from pyinaturalist.controllers import ObservationController, ProjectController, TaxonController
from pyinaturalist.controllers import (
ObservationController,
ProjectController,
TaxonController,
UserController,
)
from pyinaturalist.models import T
from pyinaturalist.paginator import Paginator
from pyinaturalist.request_params import get_valid_kwargs, strip_empty_values
Expand Down Expand Up @@ -102,6 +107,7 @@ def __init__(
self.observations = ObservationController(self) #: Interface for observation 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

@property
def access_token(self):
Expand Down
3 changes: 2 additions & 1 deletion pyinaturalist/controllers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Controller classes for :py:class:`.iNatClient`."""
"""Controller classes for :py:class:`.iNatClient`"""
# flake8: noqa: F401
from pyinaturalist.controllers.base import BaseController
from pyinaturalist.controllers.observations import ObservationController
from pyinaturalist.controllers.projects import ProjectController
from pyinaturalist.controllers.taxa import TaxonController
from pyinaturalist.controllers.users import UserController
2 changes: 1 addition & 1 deletion pyinaturalist/controllers/observations.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
# TODO: Batch from_id requests if max GET URL length is exceeded
# TODO: Consistent naming for /{id} requests. from_id(), id(), by_id(), other?
class ObservationController(BaseController):
""":fa:`binoculars` Controller for observation requests"""
""":fa:`binoculars` Controller for Observation requests"""

def from_id(self, *observation_ids, **params) -> Paginator[Observation]:
"""Get observations by ID
Expand Down
2 changes: 1 addition & 1 deletion pyinaturalist/controllers/projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@


class ProjectController(BaseController):
""":fa:`users` Controller for project requests"""
""":fa:`users` Controller for Project requests"""

def from_id(self, *project_ids, **params) -> Paginator[Project]:
"""Get projects by ID
Expand Down
2 changes: 1 addition & 1 deletion pyinaturalist/controllers/taxa.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


class TaxonController(BaseController):
""":fa:`dove,style=fas` Controller for taxon requests"""
""":fa:`dove,style=fas` Controller for Taxon requests"""

def from_id(self, *taxon_ids, **params) -> Paginator[Taxon]:
"""Get taxa by ID
Expand Down
26 changes: 26 additions & 0 deletions pyinaturalist/controllers/users.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import Iterator, List
from pyinaturalist.controllers import BaseController
from pyinaturalist.docs import document_controller_params
from pyinaturalist.models import User

from pyinaturalist.v1 import get_user_by_id, get_users_autocomplete


class UserController(BaseController):
""":fa:`user` Controller for User requests"""

# TODO: Paginator subclass for this?
def from_id(self, *user_ids, **params) -> Iterator[User]:
"""Get users by ID
Args:
user_ids: One or more project IDs
"""
for user_id in user_ids:
response = get_user_by_id(user_id, **params)
yield User.from_json(response)

@document_controller_params(get_users_autocomplete)
def autocomplete(self, **params) -> List[User]:
response = get_users_autocomplete(**params)
return User.from_json_list(response)
8 changes: 8 additions & 0 deletions pyinaturalist/paginator.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
logger = getLogger(__name__)


# TODO: ID-based paginator for endpoints that only accept a single ID per request (like /users/{id})
# TODO: Add per-endpoint 'max_per_page' parameter to use with Paginator.all()
class Paginator(Iterable, AsyncIterable, Generic[T]):
"""Class to handle pagination of API requests, with async support
Expand Down Expand Up @@ -96,6 +98,12 @@ def all(self) -> List[T]:
"""Get all results in a single list"""
return list(self)

def one(self) -> T:
"""Get the first result from the query"""
self.per_page = 1
results = self.next_page()
return self.model.from_json(results[0])

def count(self) -> int:
"""Get the total number of results for this query, without fetching response data.
Expand Down
2 changes: 1 addition & 1 deletion pyinaturalist/v1/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def get_user_by_id(user_id: int, **params) -> JsonResponse:
Returns:
Response dict containing user record
"""
response = get_v1('users', ids=[user_id], **params)
response = get_v1('users', ids=user_id, **params)
results = response.json()['results']
if not results:
return {}
Expand Down
31 changes: 31 additions & 0 deletions test/controllers/test_user_controller.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from pyinaturalist.client import iNatClient
from pyinaturalist.constants import API_V1_BASE_URL
from pyinaturalist.models import User
from test.sample_data import SAMPLE_DATA


def test_from_id(requests_mock):
user_id = 1
requests_mock.get(
f'{API_V1_BASE_URL}/users/{user_id}',
json=SAMPLE_DATA['get_user_by_id'],
status_code=200,
)

client = iNatClient()
results = list(client.users.from_id(user_id))
assert len(results) == 1 and isinstance(results[0], User)
assert results[0].id == user_id


def test_autocomplete(requests_mock):
requests_mock.get(
f'{API_V1_BASE_URL}/users/autocomplete',
json=SAMPLE_DATA['get_users_autocomplete'],
status_code=200,
)

client = iNatClient()
results = client.users.autocomplete(q='nico')
assert len(results) == 3 and isinstance(results[0], User)
assert results[0].id == 886482

0 comments on commit 78f383f

Please sign in to comment.