Skip to content

Commit

Permalink
Merge branch 'more-fields'
Browse files Browse the repository at this point in the history
  • Loading branch information
JWCook committed Jul 4, 2021
2 parents 7535063 + 320382e commit 7d84b0e
Show file tree
Hide file tree
Showing 17 changed files with 568 additions and 80 deletions.
2 changes: 1 addition & 1 deletion examples/Data Visualizations - Seaborn.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"metadata": {},
"source": [
"# Seaborn examples\n",
"Here are some examples of visualizations that can be created using [Seaborn](seaborn.pydata.org)."
"Here are some examples of visualizations that can be created using [Seaborn](https://seaborn.pydata.org/)."
]
},
{
Expand Down
11 changes: 5 additions & 6 deletions pyinaturalist/api_docs/model_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,10 @@ def get_model_classes() -> List[Type]:
return model_classes


# TODO: Also include regular @properties?
# TODO: CSS to style LazyProperties with a different background color?
# TODO: Remove autodoc member docs for LazyProperties
def get_model_doc(cls: Type) -> List[Tuple[str, str, str]]:
"""Get the name, type and description for all model attributes, properties, and LazyProperties.
If an attribute has a validator with options, include those options in the description.
If an attribute has metadata for options (possible values for the attribute), include those
options in the description.
"""

doc_rows = [_get_field_doc(field) for field in cls.__attrs_attrs__ if not field.name.startswith('_')]
Expand All @@ -70,8 +68,9 @@ def _get_field_doc(field: Attribute) -> Tuple[str, str, str]:
"""Get a row documenting an attrs Attribute"""
rtype = format_annotation(field.type)
doc = field.metadata.get('doc', '')
if getattr(field.validator, 'options', None):
options = ', '.join([f'``{opt}``' for opt in field.validator.options if opt is not None])
options = field.metadata.get('options', [])
if options:
options = ', '.join([f'``{opt}``' for opt in filter(None, options)])
doc += f'\n\n**Options:** {options}'
return (f'**{field.name}**', rtype, doc)

Expand Down
2 changes: 1 addition & 1 deletion pyinaturalist/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@
ID_CATEGORIES = ['improving', 'supporting', 'leading', 'maverick']
ORDER_DIRECTIONS = ['asc', 'desc']
PLACE_CATEGORIES = ['standard', 'community']
PROJECT_TYPES = ['collection', 'umbrella']
PROJECT_TYPES = ['assessment', 'bioblitz', 'collection', 'umbrella']
QUALITY_GRADES = ['casual', 'needs_id', 'research']
SEARCH_PROPERTIES = ['names', 'tags', 'description', 'place']
SOURCES = ['places', 'projects', 'taxa', 'users']
Expand Down
1 change: 0 additions & 1 deletion pyinaturalist/formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,6 @@ def _format_model_objects(obj: ResponseOrResults, cls: Type[BaseModel], **kwargs
return '\n'.join([str(obj) for obj in objects])


# TODO: Figure out type annotations for these. Or just replace with pprint()?
format_controlled_terms = partial(_format_model_objects, cls=ControlledTerm)
format_identifications = partial(_format_model_objects, cls=Identification)
format_observations = partial(_format_model_objects, cls=Observation)
Expand Down
5 changes: 3 additions & 2 deletions pyinaturalist/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@
define_model_collection: Callable = define(auto_attribs=False, order=False, slots=False)


def field(doc: str = '', metadata: Dict = None, **kwargs):
"""A field with extra documentation metadata"""
def field(doc: str = '', options: Iterable = None, metadata: Dict = None, **kwargs):
"""A field with extra metadata for documentation and options"""
metadata = metadata or {}
metadata['doc'] = doc
metadata['options'] = options
return attr.field(**kwargs, metadata=metadata)


Expand Down
3 changes: 1 addition & 2 deletions pyinaturalist/models/identification.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
datetime_now_field,
define_model,
field,
is_in,
)


Expand All @@ -20,7 +19,7 @@ class Identification(BaseModel):
"""

body: str = field(default=None, doc='Comment text')
category: str = field(default=None, validator=is_in(ID_CATEGORIES), doc='Identification category')
category: str = field(default=None, options=ID_CATEGORIES, doc='Identification category')
created_at: datetime = datetime_now_field(doc='Date and time the identification was added')
current: bool = field(
default=None, doc='Indicates if the identification is the currently accepted one'
Expand Down
53 changes: 23 additions & 30 deletions pyinaturalist/models/observation.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# TODO: Possible models for faves, sounds, and votes?
from datetime import datetime
from typing import Any, Dict, List, Optional

Expand Down Expand Up @@ -30,7 +31,6 @@
datetime_now_field,
define_model_collection,
field,
is_in,
upper,
)
from pyinaturalist.v1 import get_observation
Expand All @@ -48,20 +48,16 @@ class Observation(BaseModel):
)
community_taxon_id: int = field(default=None, doc='The current community identification taxon')
description: str = field(default=None, doc='Observation description')
geoprivacy: str = field(
default=None, validator=is_in(GEOPRIVACY_LEVELS), doc='Location privacy level'
)
faves: List[Dict] = field(factory=list, doc='Details on users who have favorited the observation')
geoprivacy: str = field(default=None, options=GEOPRIVACY_LEVELS, doc='Location privacy level')
identifications_count: int = field(default=None, doc='Total number of identifications')
identifications_most_agree: bool = field(default=None, doc='Indicates if most identifications agree')
identifications_most_disagree: bool = field(
default=None, doc='Indicates if most identifications disagree'
)
identifications_some_agree: bool = field(default=None, doc='Indicates if some identifications agree')
license_code: str = field(
default=None,
converter=upper,
validator=is_in(ALL_LICENSES),
doc='Creative Commons license code',
default=None, converter=upper, options=ALL_LICENSES, doc='Creative Commons license code'
)
location: Coordinates = coordinate_pair()
mappable: bool = field(default=None, doc='Indicates if the observation can be shown on a map')
Expand All @@ -74,38 +70,22 @@ class Observation(BaseModel):
default=None,
doc='Indicates if coordinates are obscured (showing a broad approximate location on the map)',
)
observed_on: DateTime = datetime_field(doc='Date and time the organism was observed')
outlinks: List[Dict] = field(
factory=list, doc='Linked observation pages on other sites (e.g., GBIF)'
)
out_of_range: bool = field(
default=None, doc='Indicates if the taxon is observed outside of its known range'
)
owners_identification_from_vision: bool = field(
default=None, doc="Indicates if the owner's ID was selected from computer vision results"
)

place_guess: str = field(default=None, doc='Place name determined from observation coordinates')
place_ids: List[int] = field(factory=list)
positional_accuracy: int = field(
default=None, doc='GPS accuracy in meters (real accuracy, if obscured)'
)
public_positional_accuracy: int = field(
default=None, doc='GPS accuracy in meters (not accurate if obscured)'
)
quality_grade: str = field(default=None, validator=is_in(QUALITY_GRADES), doc='Quality grade')
site_id: int = field(
default=None, doc='Site ID for iNaturalist network members, or ``1`` for inaturalist.org'
)
species_guess: str = field(default=None, doc="Taxon name from observer's initial identification")
observed_on: DateTime = datetime_field(doc='Date and time the organism was observed')
updated_at: DateTime = datetime_field(doc='Date and time the observation was last updated')
uri: str = field(default=None, doc='Link to observation details page')
uuid: str = field(
default=None, doc='Universally unique ID; generally preferred over ``id`` where possible'
)

# Nested collections
# TODO: Possible models for faves, sounds, and votes?
faves: List[Dict] = field(factory=list, doc='Details on users who have favorited the observation')
outlinks: List[Dict] = field(
factory=list, doc='Linked observation pages on other sites (e.g., GBIF)'
)
place_ids: List[int] = field(factory=list)
preferences: Dict[str, Any] = field(
factory=dict,
doc='Any user observation preferences (related to community IDs, coordinate access, etc.)',
Expand All @@ -117,10 +97,23 @@ class Observation(BaseModel):
project_ids_without_curator_id: List[int] = field(
factory=list, doc='Project IDs without curator identification for this observation'
)
public_positional_accuracy: int = field(
default=None, doc='GPS accuracy in meters (not accurate if obscured)'
)
quality_grade: str = field(default=None, options=QUALITY_GRADES, doc='Quality grade')
quality_metrics: List[Dict] = field(factory=list, doc='Data quality assessment metrics')
reviewed_by: List[int] = field(factory=list, doc='IDs of users who have reviewed the observation')
site_id: int = field(
default=None, doc='Site ID for iNaturalist network members, or ``1`` for inaturalist.org'
)
sounds: List[Dict] = field(factory=list, doc='Observation sound files')
species_guess: str = field(default=None, doc="Taxon name from observer's initial identification")
tags: List[str] = field(factory=list, doc='Arbitrary user tags added to the observation')
updated_at: DateTime = datetime_field(doc='Date and time the observation was last updated')
uri: str = field(default=None, doc='Link to observation details page')
uuid: str = field(
default=None, doc='Universally unique ID; generally preferred over ``id`` where possible'
)
votes: List[Dict] = field(factory=list, doc='Votes on data quality assessment metrics')

# Lazy-loaded model objects
Expand Down
8 changes: 2 additions & 6 deletions pyinaturalist/models/photo.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
# TODO: Method to preview image in Jupyter
from typing import Optional, Tuple

from pyinaturalist.constants import ALL_LICENSES, CC_LICENSES, PHOTO_INFO_BASE_URL, PHOTO_SIZES, TableRow
from pyinaturalist.converters import format_dimensions, format_license
from pyinaturalist.models import BaseModel, define_model, field, is_in
from pyinaturalist.models import BaseModel, define_model, field


@define_model
Expand All @@ -14,10 +13,7 @@ class Photo(BaseModel):

attribution: str = field(default=None, doc='License attribution')
license_code: str = field(
default=None,
converter=format_license,
validator=is_in(ALL_LICENSES),
doc='Creative Commons license code',
default=None, converter=format_license, options=ALL_LICENSES, doc='Creative Commons license code'
)
original_dimensions: Tuple[int, int] = field(
converter=format_dimensions, default=(0, 0), doc='Dimensions of original image'
Expand Down
30 changes: 19 additions & 11 deletions pyinaturalist/models/project.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
from datetime import datetime
from typing import Dict, List

from pyinaturalist.constants import INAT_BASE_URL, Coordinates, DateTime, JsonResponse, TableRow
from pyinaturalist.constants import (
INAT_BASE_URL,
PROJECT_TYPES,
Coordinates,
DateTime,
JsonResponse,
TableRow,
)
from pyinaturalist.models import (
BaseModel,
LazyProperty,
Expand Down Expand Up @@ -81,21 +88,22 @@ class Project(BaseModel):
doc='Indicates if this is an umbrella project (containing observations from other projects)',
)
location: Coordinates = coordinate_pair()
place_id: int = field(default=None)
prefers_user_trust: bool = field(default=None)
project_type: str = field(default=None) # Enum
slug: str = field(default=None, doc='URL slug')
terms: str = field(default=None, doc='Project terms')
title: str = field(default=None, doc='Project title')
updated_at: DateTime = datetime_field(doc='Date and time the project was last updated')

# Nested collections
project_observation_rules: List[Dict] = field(factory=list)
place_id: int = field(default=None, doc='Project place ID')
prefers_user_trust: bool = field(
default=None,
doc='Indicates if the project wants users to share hidden coordinates with the project admins',
)
project_observation_rules: List[Dict] = field(factory=list, doc='Project observation rules')
project_type: str = field(default=None, options=PROJECT_TYPES, doc='Project type') # Enum
rule_preferences: List[Dict] = field(factory=list)
search_parameters: List[Dict] = field(factory=list, doc='Filters for observations to include')
site_features: List[Dict] = field(
factory=list, doc='Details about if/when the project was featured on inaturalist.org'
)
slug: str = field(default=None, doc='URL slug')
terms: str = field(default=None, doc='Project terms')
title: str = field(default=None, doc='Project title')
updated_at: DateTime = datetime_field(doc='Date and time the project was last updated')
user_ids: List[int] = field(factory=list)

# Lazy-loaded model objects
Expand Down
4 changes: 2 additions & 2 deletions pyinaturalist/models/search.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from typing import List, Union

from pyinaturalist.constants import TableRow
from pyinaturalist.models import BaseModel, Place, Project, Taxon, User, define_model, field, is_in
from pyinaturalist.models import BaseModel, Place, Project, Taxon, User, define_model, field

SEARCH_RESULT_TYPES = {cls.__name__: cls for cls in [Place, Project, Taxon, User]}
SEARCH_RESULT_TITLES = {'Place': 'name', 'Project': 'title', 'Taxon': 'full_name', 'User': 'login'}
Expand All @@ -15,7 +15,7 @@ class SearchResult(BaseModel):
"""

score: float = field(default=0, doc='Search result rank')
type: str = field(default=None, validator=is_in(SEARCH_RESULT_TYPES), doc='Search result type')
type: str = field(default=None, options=SEARCH_RESULT_TYPES, doc='Search result type')
matches: List[str] = field(factory=list, doc='Search terms matched')
record: SearchResultRecord = field(default=None, doc='Search result object')

Expand Down
29 changes: 15 additions & 14 deletions pyinaturalist/models/taxon.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@
define_model,
define_model_collection,
field,
is_in,
upper,
)
from pyinaturalist.v1 import get_taxa_by_id
Expand Down Expand Up @@ -52,7 +51,7 @@ class ConservationStatus(BaseModel):
status: str = field(
default=None,
converter=upper,
validator=is_in(CONSERVATION_STATUSES),
options=CONSERVATION_STATUSES,
doc='Short code for conservation status',
)
status_name: str = field(default=None, doc='Full name of conservation status')
Expand All @@ -78,7 +77,7 @@ class EstablishmentMeans(BaseModel):
"""The establishment means for a taxon in a given location"""

establishment_means: str = field(
default=None, validator=is_in(ESTABLISTMENT_MEANS), doc='Establishment means description'
default=None, options=ESTABLISTMENT_MEANS, doc='Establishment means description'
)
place: property = LazyProperty(
Place.from_json_list, type=Place, doc='Location that the establishment means applies to'
Expand All @@ -97,13 +96,19 @@ class Taxon(BaseModel):
include nested ``ancestors``, ``children``, and results from :py:func:`.get_taxa_autocomplete`.
"""

ancestor_ids: List[int] = field(
factory=list, doc='Taxon IDs of ancestors, from highest rank to lowest'
)
complete_rank: str = field(
default=None, doc='Complete or "leaf taxon" rank, e.g. species or subspecies'
)
complete_species_count: int = field(
default=None, doc='Total number of species descended from this taxon'
)
created_at: DateTime = datetime_field(doc='Date and time the taxon was added to iNaturalist')
current_synonymous_taxon_ids: List[int] = field(
factory=list, doc='Taxon IDs of taxa that are accepted synonyms'
)
extinct: bool = field(default=None, doc='Indicates if the taxon is extinct')
iconic_taxon_id: int = field(
default=0, doc='ID of the iconic taxon (e.g., general taxon "category")'
Expand All @@ -114,12 +119,17 @@ class Taxon(BaseModel):
is_active: bool = field(
default=None, doc='Indicates if the taxon is active (and not renamed, moved, etc.)'
)
listed_taxa: List[int] = field(factory=list, doc='Listed taxon IDs')
listed_taxa_count: int = field(
default=None, doc='Number of listed taxa from this taxon + descendants'
)
matched_term: str = field(default=None, doc='Matched search term, from autocomplete results')
name: str = field(
default=None, doc='Taxon name; contains full scientific name at species level and below'
)
names: List[Dict] = field(
factory=list, doc='All regional common names; only returned if ``all_names`` is specified'
)
observations_count: int = field(
default=None, doc='Total number of observations of this taxon and its descendants'
)
Expand All @@ -132,24 +142,15 @@ class Taxon(BaseModel):
default=None,
doc='Number indicating rank level, for easier comparison between ranks (kingdom=higest)',
)
rank: str = field(default=None, validator=is_in(RANKS), doc='Taxon rank')
rank: str = field(default=None, options=RANKS, doc='Taxon rank')
taxon_changes_count: int = field(default=None, doc='Number of curator changes to this taxon')
taxon_schemes_count: int = field(default=None, doc='Taxon schemes that include this taxon')
wikipedia_summary: str = field(
default=None, doc='Taxon summary from Wikipedia article, if available'
)
wikipedia_url: str = field(default=None, doc='URL to Wikipedia article for the taxon, if available')

# Nested collections
ancestor_ids: List[int] = field(
factory=list, doc='Taxon IDs of ancestors, from highest rank to lowest'
)
current_synonymous_taxon_ids: List[int] = field(
factory=list, doc='Taxon IDs of taxa that are accepted synonyms'
)
listed_taxa: List[int] = field(factory=list, doc='Listed taxon IDs')

# Lazy-loade model objects
# Lazy-loaded model objects
ancestors: property = LazyProperty(BaseModel.from_json_list)
children: property = LazyProperty(BaseModel.from_json_list)
conservation_status: property = LazyProperty(
Expand Down
1 change: 0 additions & 1 deletion pyinaturalist/node_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ def get_all_observations(**params) -> List[JsonResponse]:
return paginate_all(get_observations, method='id', **params)['results']


# TODO: Deprecate get_geojson_observations and move to pyinaturalist-convert
def get_geojson_observations(properties: List[str] = None, **params) -> JsonResponse:
"""Get all observation results combined into a GeoJSON ``FeatureCollection``.
By default this includes some basic observation properties as GeoJSON ``Feature`` properties.
Expand Down
1 change: 0 additions & 1 deletion pyinaturalist/request_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
The main purpose of these functions is to support some python-specific conveniences and translate
them into standard request parameters, along with request validation that makes debugging easier.
"""
# TODO: It would be nice to put all the multiple-choice options on the models and use attrs validators
from datetime import date, datetime
from dateutil.relativedelta import relativedelta
from logging import getLogger
Expand Down
1 change: 0 additions & 1 deletion pyinaturalist/v0/observations.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
# TODO: Decide on consistent terminology for POST/PUT endpoints, rename, and add DeprecationWarnings to previous names
from logging import getLogger
from typing import List, Union

Expand Down
Loading

0 comments on commit 7d84b0e

Please sign in to comment.