Skip to content

Commit

Permalink
WIP: Add documented + annotated request params for REST API POST and …
Browse files Browse the repository at this point in the history
…PUT /observations endpoints
  • Loading branch information
JWCook committed Sep 1, 2020
1 parent c1a5cd9 commit 975d49f
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 23 deletions.
101 changes: 97 additions & 4 deletions pyinaturalist/api_docs.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# TODO: Consistent naming for template functions
"""
Reusable template functions + utilities used for API documentation.
Each template function contains a portion of an endpoint's request parameters, with corresponding
Expand Down Expand Up @@ -34,7 +35,7 @@
from itertools import chain
from functools import wraps
from logging import getLogger
from typing import Callable, Dict, List
from typing import Any, Callable, Dict, List

from pyinaturalist.constants import (
MultiInt,
Expand Down Expand Up @@ -183,7 +184,6 @@ def observation_params_common(

# Observation params that are only in the Node API
def observation_params_node_only(
params: Dict = None,
acc: bool = None,
captive: bool = None,
endemic: bool = None,
Expand Down Expand Up @@ -247,7 +247,6 @@ def observation_params_node_only(
ttl: str = None,
):
"""
ofv_datatype: Must have an observation field value with this datatype
acc: Whether or not positional accuracy / coordinate uncertainty has been specified
captive: Captive or cultivated observations
endemic: Observations whose taxa are endemic to their location
Expand All @@ -274,6 +273,7 @@ def observation_params_node_only(
project_id: Must be added to the project this ID or slug
rank: Taxon must have this rank
site_id: Must be affiliated with the iNaturalist network website with this ID
ofv_datatype: Must have an observation field value with this datatype
sound_license: Must have at least one sound with this license
without_taxon_id: Exclude observations of these taxa and their descendants
user_id: User must have this ID or login
Expand Down Expand Up @@ -350,6 +350,67 @@ def observation_params_rest_only(
"""


# TODO: Are array params (e.g. `flickr_photos[]`) required to have "[]" in the param name?
def _create_observations_params(
species_guess: str = None,
taxon_id: int = None,
observed_on_string: Date = None,
time_zone: str = None,
description: str = None,
tag_list: MultiStr = None,
place_guess: str = None,
latitude: float = None,
longitude: float = None,
map_scale: int = None,
positional_accuracy: int = None,
geoprivacy: str = None,
# observation_field_values_attributes[order]:
flickr_photos: MultiInt = None,
picasa_photos=None,
facebook_photos=None,
local_photos=None,
):
"""
species_guess: Equivalent to the "What did you see?" field on the observation form.
iNat will try to choose a single taxon based on this, but it may fail if it's ambuguous
taxon_id: ID of the taxon to associate with this observation
observed_on_string: Date/time of the observation. Time zone will default to the user's
time zone if not specified.
time_zone: Time zone the observation was made in
description: Observation description
tag_list: Comma-separated list of tags
place_guess: Name of the place where the observation was recorded.
**Note:** iNat will *not* try to automatically look up coordinates based on this string
latitude: Latitude of the observation; presumed datum is **WGS84**
longitude: Longitude of the observation; presumed datum is **WGS84**
map_scale: Google Maps zoom level (from **0 to 19**) at which to show this observation's map marker.
positional_accuracy: Positional accuracy of the observation coordinates, in meters
geoprivacy: Geoprivacy for the observation
observation_field_values_attributes[order]: [NOT IMPLEMENTED] Nested fields for observation field values.
``order`` is just an integer starting with zero specifying the order of entry.
flickr_photos: Flickr photo ID(s) to add as photos for this observation. User must have
their Flickr and iNat accounts connected, and the user must own the photo(s) on Flickr.
picasa_photos: Picasa photo ID(s) to add as photos for this observation. User must have
their Picasa and iNat accounts connected, and the user must own the photo(s) on Picasa.
facebook_photos: Facebook photo IDs to add as photos for this observation. User must have
their Facebook and iNat accounts connected, and the user must own the photo on Facebook.
local_photos: Fields containing uploaded photo data. Request must have a ``Content-Type``
of ``"multipart"``. We recommend that you use the ``POST /observation_photos`` endpoint
instead.
"""


def _update_observation_params(
# _method: str = None, # Exposed as a client-specific workaround; not needed w/ `requests`
ignore_photos: bool = False,
):
"""
ignore_photos
If photos exist on the observation but are missing in the request, simpy ignore them
instead of deleting the missing observation photos
"""


def taxon_params(
q: str = None,
is_active: bool = None,
Expand Down Expand Up @@ -398,6 +459,13 @@ def taxon_id_params(
# ------------------------


def access_token(access_token: str = None):
"""
access_token: An access token required for user authentication, as returned by
:py:func:`get_access_token()`
"""


def bounding_box(
nelat: float = None,
nelng: float = None,
Expand All @@ -418,6 +486,12 @@ def geojson_properties(properties: List[str] = None):
"""


def legacy_params(params: Dict[str, Any] = None):
"""
params: [DEPRECATED] Request parameters as a dict instead of keyword arguments
"""


def minify(minify: str = None):
"""
minify: Condense each match into a single string containg taxon ID, rank, and name
Expand All @@ -436,6 +510,12 @@ def only_id(only_id: bool = False):
"""


def observation_id(observation_id: int):
"""
observation_id: iNaturalist observation ID to update
"""


def page(page: int = None):
"""
page: Page number of results to return
Expand Down Expand Up @@ -478,7 +558,12 @@ def _format_param_choices():


# Request param combinations for Node API endpoints
_get_observations = [observation_params_common, observation_params_node_only, bounding_box]
_get_observations = [
legacy_params,
observation_params_common,
observation_params_node_only,
bounding_box,
]
get_observations_params = _get_observations + [pagination, only_id]
get_all_observations_params = _get_observations + [only_id]
get_observation_species_counts_params = _get_observations
Expand All @@ -496,6 +581,14 @@ def _format_param_choices():
]
get_observation_fields_params = [search_query, page]
get_all_observation_fields_params = [search_query]
create_observations_params = [legacy_params, access_token, _create_observations_params]
update_observation_params = [
observation_id,
legacy_params,
access_token,
_create_observations_params,
_update_observation_params,
]


MULTIPLE_CHOICE_PARAM_DOCS = "**Multiple-Choice Parameters:**\n" + _format_param_choices()
1 change: 1 addition & 0 deletions pyinaturalist/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# TODO: Make these extend `requests.HTTPError` to simplify error handling in client code?
class AuthenticationError(Exception):
pass

Expand Down
37 changes: 18 additions & 19 deletions pyinaturalist/rest_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
get_observations_params_rest as get_observations_params,
get_observation_fields_params,
get_all_observation_fields_params,
create_observations_params,
update_observation_params,
)
from pyinaturalist.constants import OBSERVATION_FORMATS, THROTTLING_DELAY, INAT_BASE_URL
from pyinaturalist.exceptions import AuthenticationError, ObservationNotFound
Expand Down Expand Up @@ -227,8 +229,10 @@ def add_photo_to_observation(
return response.json()


# TODO: Implement `observation_field_values_attributes`, and simplify nested data structures
@document_request_params(create_observations_params)
def create_observations(
params: Dict[str, Dict[str, Any]], access_token: str, user_agent: str = None
params: Dict[str, Dict[str, Any]], access_token: str, user_agent: str = None, **kwargs
) -> List[Dict[str, Any]]:
"""Create one or more observations.
For API reference, see: https://www.inaturalist.org/pages/api+reference#post-observations
Expand All @@ -238,22 +242,22 @@ def create_observations(
>>> token = get_access_token('...')
>>> create_observations(params=params, access_token=token)
Args:
params:
access_token: the access token, as returned by :func:`get_access_token()`
user_agent: a user-agent string that will be passed to iNaturalist.
Returns:
The newly created observation(s) in JSON format
Raises:
:py:exc:`requests.HTTPError`, if the call is not successful. iNaturalist returns an error 422 (unprocessable entity)
if it rejects the observation data (for example an observation date in the future or a latitude > 90. In
that case the exception's `response` attribute give details about the errors.
:py:exc:`requests.HTTPError`, if the call is not successful. iNaturalist returns an
error 422 (unprocessable entity) if it rejects the observation data (for example an
observation date in the future or a latitude > 90. In that case the exception's
`response` attribute give details about the errors.
TODO investigate: according to the doc, we should be able to pass multiple observations (in an array, and in
renaming observation to observations, but as far as I saw they are not created (while a status of 200 is returned)
"""
# This is the one Boolean parameter that's specified as an int, for some reason
if "ignore_photos" in kwargs:
kwargs["ignore_photos"] = int(kwargs["ignore_photos"])

response = post(
url="{base_url}/observations.json".format(base_url=INAT_BASE_URL),
json=params,
Expand All @@ -264,28 +268,23 @@ def create_observations(
return response.json()


@document_request_params(update_observation_params)
def update_observation(
observation_id: int,
params: Dict[str, Any],
access_token: str,
user_agent: str = None,
observation_id: int, params: Dict[str, Any], access_token: str, user_agent: str = None, **kwargs
) -> List[Dict[str, Any]]:
"""
Update a single observation. See https://www.inaturalist.org/pages/api+reference#put-observations-id
Args:
observation_id: the ID of the observation to update
params: to be passed to iNaturalist API
access_token: the access token, as returned by :func:`get_access_token()`
user_agent: a user-agent string that will be passed to iNaturalist.
Returns:
iNaturalist's JSON response, as a Python object
Raises:
:py:exc:`requests.HTTPError`, if the call is not successful. iNaturalist returns an
error 410 if the observation doesn't exists or belongs to another user.
"""
if "ignore_photos" in kwargs:
kwargs["ignore_photos"] = int(kwargs["ignore_photos"])

response = put(
url="{base_url}/observations/{id}.json".format(base_url=INAT_BASE_URL, id=observation_id),
json=params,
Expand Down

0 comments on commit 975d49f

Please sign in to comment.