class ControlledTermCount(BaseModel): """:fa:`tag` A count + histogram of a controlled term and value""" count: int = field(default=0, doc='') histogram: Dict[int, int] = field(factory=dict) term = LazyProperty(ControlledTerm.from_json, type=ControlledTerm) value = LazyProperty(ControlledTermValue.from_json, type=ControlledTermValue) @classmethod def from_json(cls, value: JsonResponse, user_id: int = None, **kwargs) -> 'ControlledTermCount': """Rename some response fields before initializing""" value['histogram'] = value.pop('month_of_year', None) value['term'] = value.pop('controlled_attribute', None) value['value'] = value.pop('controlled_value', None) return super(ControlledTermCount, cls).from_json(value) @property def term_label(self) -> str: return self.term.label @property def value_label(self) -> str: return self.value.label @property def _row(self) -> TableRow: return { 'Term': self.term.label, 'Value': self.value.label, 'Count': self.count, } @property def _str_attrs(self) -> List[str]: return ['term_label', 'value_label', 'count']
class Message(BaseModel): """:fa:`user` A message from the user's inbox, based on the schema of :v1:`GET /messages <Messages/get_messages>` """ user_id: int = field(default=None, doc='Corresponding user ID') thread_id: int = field(default=None, doc='Message thread ID') subject: str = field(default=None, doc='Message subject') body: str = field(default=None, doc='Message body') read_at: DateTime = datetime_field(doc='When the message was read') created_at: DateTime = datetime_field(doc='When the message was sent') updated_at: DateTime = datetime_field( doc='When the message was last edited') thread_flags: List[str] = field(factory=list) thread_messages_count: int = field(default=None, doc='Number of messages in the thread') from_user: property = LazyProperty(User.from_json, type=User, doc='Message sender') to_user: property = LazyProperty(User.from_json, type=User, doc='Message recipient') @property def truncated_body(self): """Comment text, truncated""" truncated_body = self.body.replace('\n', ' ').strip() if len(truncated_body) > 50: truncated_body = truncated_body[:47].strip() + '...' return truncated_body @property def _row(self) -> TableRow: return { 'ID': self.id, 'Date': self.created_at, 'To': self.to_user.login, 'From': self.from_user.login, 'Subject': self.subject, } @property def _str_attrs(self) -> List[str]: return [ 'id', 'created_at', 'self.to_user', 'self.from_user', 'subject', 'truncated_body' ] def __str__(self) -> str: return ( f'[{self.id}] Sent {self.created_at.strftime(DATETIME_SHORT_FORMAT)} ' f'from {self.from_user.login} to {self.to_user.login}: {self.subject}' )
class ConservationStatus(IdWrapperMixin, BaseModel): """:fa:`exclamation-triangle` The conservation status of a taxon in a given location, based on the schema of: * ``Taxon.conservation_status`` from :v1:`GET /taxa <Taxa/get_taxa>` * ``Observation.taxon.conservation_statuses`` from :v1:`GET /observations <Observations/get_observations>` * ``conservation_status`` from :v1:`GET /observation/{id}/taxon_summary <Observations/get_observations_id_taxon_summary>` """ authority: str = field(default=None, doc='Data source for conservation status') created_at: DateTime = datetime_field( doc='Date and time the record was created') description: str = field(default=None, doc='Description of conservation status') geoprivacy: str = field( default=None, doc= 'Default geoprivacy level; may be obscured or private for protected species', ) iucn: int = field(default=None, doc='IUCD ID, if applicable') source_id: int = field(default=None) status: str = field( default=None, converter=upper, options=CONSERVATION_STATUSES, doc='Short code for conservation status', ) status_name: str = field(default=None, doc='Full name of conservation status') taxon_id: int = field(default=None, doc='Taxon ID') updated_at: DateTime = datetime_field( doc='Date and time the record was last updated') url: str = field(default=None, doc='Link to data source with more details') # Lazy-loaded nested model objects place: property = LazyProperty( # type: ignore Place.from_json, type=Place, doc='Location that the conservation status applies to') updater: User = LazyProperty( User.from_json, type=User, doc='User that last updated the record') # type: ignore user: User = LazyProperty( User.from_json, type=User, doc='User that created the record') # type: ignore # TODO: Are these needed? They appear to be redundant with `status` and `status_name` # iucn_status: str = field(default=None) # iucn_status_code: str = field(default=None) @property def _str_attrs(self) -> List[str]: return ['status_name', 'status', 'authority']
class ControlledTerm(BaseModel): """:fa:`tag` A controlled term, based on the schema of `GET /controlled_terms <https://api.inaturalist.org/v1/docs/#!/Controlled_Terms/get_controlled_terms>`_. """ is_value: bool = field(default=None) multivalued: bool = field(default=None) label: str = field(default=None) ontology_uri: str = field(default=None) uri: str = field(default=None) uuid: str = field(default=None) taxon_ids: List[int] = field(factory=list) values: property = LazyProperty( ControlledTermValue.from_json_list, type=List[ControlledTermValue], doc='Allowed values for this controlled term', ) @property def value_labels(self) -> str: """Combined labels from all controlled term values""" return ', '.join([value.label for value in self.values]) @property def _row(self) -> TableRow: return { 'ID': self.id, 'Label': self.label, 'Values': self.value_labels, } @property def _str_attrs(self) -> List[str]: return ['id', 'label', 'value_labels']
class Annotation(BaseModel): """:fa:`tag` An annotation, meaning a **controlled term value** applied by a **user** to an **observation**. Based on the schema of annotations from `GET /observations <https://api.inaturalist.org/v1/docs/#!/Observations/get_observations>`_. """ concatenated_attr_val: str = field(default=None) controlled_attribute_id: int = field(default=None) controlled_value_id: int = field(default=None) user_id: int = field(default=None) uuid: str = field(default=None) vote_score: int = field(default=None) votes: List = field(factory=list) user: property = LazyProperty(User.from_json, type=User, doc='User who added the annotation') @property def values(self) -> List[str]: """Split pipe-delimited annotation values into separate tokens""" if not self.concatenated_attr_val: return [] return self.concatenated_attr_val.split('|') @property def _row(self) -> TableRow: return { 'ID': self.controlled_attribute_id, 'Values': ', '.join(self.values), 'Votes': self.vote_score, 'User': self.user.login, } @property def _str_attrs(self) -> List[str]: return ['controlled_attribute_id', 'concatenated_attr_val']
class EstablishmentMeans(BaseModel): """:fa:`exclamation-triangle` The establishment means for a taxon in a given location""" establishment_means: str = field( default=None, options=ESTABLISTMENT_MEANS, converter=lambda x: str(x).lower(), doc='Establishment means label', ) establishment_means_description: str = field( default=None, options=ESTABLISTMENT_MEANS, doc='Establishment means description') place: property = LazyProperty( Place.from_json, type=Place, doc='Location that the establishment means applies to') # TODO: Is establishment_means_label ever different from establishment_means? @property def establishment_means_label(self) -> str: return self.establishment_means @establishment_means_label.setter def establishment_means_label(self, value: str): self.establishment_means = value def __str__(self) -> str: return f'EstablishmentMeans({self.establishment_means})'
class ObservationFieldValue(BaseModel): """:fa:`tag` An observation field **value**, based on the schema of ``Observation.ofvs`` from `GET /observations <https://api.inaturalist.org/v1/docs/#!/Observations/get_observations>`_. """ datatype: str = field(default=None) # Enum field_id: int = field(default=None) name: str = field(default=None) taxon_id: int = field(default=None) user_id: int = field(default=None) uuid: str = field(default=None) value: OFVValue = field(default=None) taxon: property = LazyProperty( Taxon.from_json, type=Taxon, doc='Taxon that the observation field applies to' ) user: property = LazyProperty( User.from_json, type=User, doc='User that applied the observation field value' ) # Unused attrbiutes # name_ci: str = field(default=None) # value_ci: int = field(default=None) # Convert value by datatype def __attrs_post_init__(self): if self.datatype in OFV_DATATYPES and self.value is not None: converter = OFV_DATATYPES[self.datatype] try: self.value = converter(self.value) except ValueError: self.value = None @property def _row(self) -> TableRow: return { 'ID': self.id, 'Type': self.datatype, 'Name': self.name, 'Value': self.value, } @property def _str_attrs(self) -> List[str]: return ['id', 'datatype', 'name', 'value']
class TaxonSummary(BaseModel): """:fa:`dove,style=fas` :fa:`list` Information about an observation's taxon, within the context of the observation's location. Based on the schema of :v1:`GET /observations/{id}/taxon_summary <Observations/get_observations_id_taxon_summary>` """ conservation_status: property = LazyProperty( ConservationStatus.from_json, type=ConservationStatus, doc='Conservation status of the taxon in the observation location', ) listed_taxon: property = LazyProperty( ListedTaxon.from_json, type=ListedTaxon, doc='Details about the taxon on an "original" life list', ) wikipedia_summary: str = field(default=None, doc='Taxon summary from Wikipedia article') @property def _str_attrs(self) -> List[str]: return ['conservation_status', 'listed_taxon']
class ProjectObservation(BaseModel): """:fa:`binoculars` Metadata about an observation that has been added to a project""" preferences: Dict = field(factory=dict) # Example: {'allows_curator_coordinate_access': True} project: Dict = field(factory=dict) # Example: {'id': 24237} user_id: int = field(default=None) uuid: str = field(default=None, doc='Universally unique identifier') user: property = LazyProperty( User.from_json, type=User, doc='User that added the observation to the project' ) @property def _str_attrs(self) -> List[str]: return ['project', 'user_id']
class Comment(BaseModel): """:fa:`comment` An observation comment, based on the schema of comments from `GET /observations <https://api.inaturalist.org/v1/docs/#!/Observations/get_observations>`_. """ body: str = field(default='', doc='Comment text') created_at: datetime = datetime_now_field( doc='Date and time the comment was created') hidden: bool = field(default=None, doc='Indicates if the comment is hidden') uuid: str = field(default=None, doc='Universally unique identifier') user: property = LazyProperty(User.from_json, type=User, doc='User that added the comment') # Unused attributes # created_at_details: Dict = field(factory=dict) # flags: List = field(factory=list) # moderator_actions: List = field(factory=list) @property def truncated_body(self): """Comment text, truncated""" truncated_body = self.body.replace('\n', ' ').strip() if len(truncated_body) > 50: truncated_body = truncated_body[:47].strip() + '...' return truncated_body @property def username(self) -> str: return self.user.login @property def _row(self) -> TableRow: return { 'ID': self.id, 'User': self.username, 'Created at': self.created_at, 'Comment': self.truncated_body, } @property def _str_attrs(self) -> List[str]: return ['id', 'username', 'created_at', 'truncated_body']
class Project(BaseModel): """:fa:`users` An iNaturalist project, based on the schema of `GET /projects <https://api.inaturalist.org/v1/docs/#!/Projects/get_projects>`_. """ banner_color: str = field(default=None) created_at: datetime = datetime_now_field(doc='Date and time the project was created') description: str = field(default=None, doc='Project description') header_image_url: str = field(default=None) hide_title: bool = field(default=None) icon: str = field(default=None, doc='URL for project icon') is_umbrella: bool = field( default=None, doc='Indicates if this is an umbrella project (containing observations from other projects)', ) location: Coordinates = coordinate_pair() 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 admins: property = LazyProperty( ProjectUser.from_json_list, type=List[User], doc='Project admin users' ) project_observation_fields: property = LazyProperty( ProjectObservationField.from_json_list, type=List[ProjectObservationField], doc='Observation fields used by the project', ) user: property = LazyProperty(User.from_json, type=User, doc='User that created the project') # Unused attributes # flags: List = field(factory=list) # header_image_contain: bool = field(default=None) # header_image_file_name: str = field(default=None) # icon_file_name: str = field(default=None) # latitude: float = field(default=None) # longitude: float = field(default=None) # user_id: int = field(default=None) # Aliases @property def obs_fields(self): return self.project_observation_fields @property def obs_rules(self): return self.project_observation_rules @property def url(self) -> str: """Info URL on iNaturalist.org""" return f'{INAT_BASE_URL}/projects/{self.id}' @property def _row(self) -> TableRow: return { 'ID': self.id, 'Title': self.title, 'Type': self.project_type, 'URL': self.url, } @property def _str_attrs(self) -> List[str]: return ['id', 'title']
class Observation(BaseModel): """:fa:`binoculars` An observation, based the schema of :v1:`GET /observations <Observations/get_observations>` """ created_at: datetime = datetime_now_field( doc='Date and time the observation was created') captive: bool = field( default=None, doc='Indicates if the organism is non-wild (captive or cultivated)') community_taxon_id: int = field( default=None, doc='The current community identification taxon') description: str = field(default=None, doc='Observation description') 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 with the community ID') identifications_most_disagree: bool = field( default=None, doc='Indicates if most identifications disagree with the community ID') identifications_some_agree: bool = field( default=None, doc='Indicates if some identifications agree with the community ID') license_code: str = field(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') num_identification_agreements: int = field( default=None, doc='Total identifications that agree with the community ID') num_identification_disagreements: int = field( default=None, doc='Total identifications that disagree with the community ID') oauth_application_id: str = field( default=None, doc='ID of the OAuth application used to create the observation, if any' ) obscured: bool = field( 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, doc='Place IDs associated with the observation coordinates') positional_accuracy: int = field( default=None, doc='GPS accuracy in meters (real accuracy, if obscured)') preferences: Dict[str, Any] = field( factory=dict, doc= 'Any user observation preferences (related to community IDs, coordinate access, etc.)', ) private_location: Coordinates = coordinate_pair( doc= ':fa:`lock` Private location in ``(latitude, logitude)`` decimal degrees' ) private_place_ids: List[int] = field( factory=list, doc= ':fa:`lock` Place IDs associated with the private observation coordinates' ) private_place_guess: str = field( default=None, doc= ':fa:`lock` Place name determined from private observation coordinates' ) project_ids: List[int] = field(factory=list, doc='All associated project IDs') project_ids_with_curator_id: List[int] = field( factory=list, doc='Project IDs with a curator identification for this observation') 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 annotations: property = LazyProperty(Annotation.from_json_list, type=List[Annotation], doc='Observation annotations') comments: property = LazyProperty(Comment.from_json_list, type=List[Comment], doc='Observation comments') identifications: property = LazyProperty(Identification.from_json_list, type=List[Identification], doc='Observation identifications') ofvs: property = LazyProperty( ObservationFieldValue.from_json_list, type=List[ObservationFieldValue], doc='Observation field values', ) photos: property = LazyProperty(Photo.from_json_list, type=List[Photo], doc='Observation photos') project_observations: property = LazyProperty( ProjectObservation.from_json_list, type=List[ProjectObservation], doc='Details on any projects that the observation has been added to', ) taxon: property = LazyProperty(Taxon.from_json, type=Taxon, doc='Observation taxon') user: property = LazyProperty(User.from_json, type=User, doc='Observer') # Additional attributes from API response that aren't needed; just left here for reference # cached_votes_total: int = field(default=None) # comments_count: int = field(default=None) # created_at_details: Dict = field(factory=dict) # created_time_zone: str = field(default=None) # faves_count: int = field(default=None) # flags: List = field(factory=list) # geojson: Dict = field(factory=dict) # id_please: bool = field(default=None) # map_scale: int = field(default=None) # non_owner_ids: List = field(factory=list) # observed_on_details: Dict = field(factory=dict) # observed_on_string: str = field(default=None) # observation_photos: List[Photo] = field(converter=Photo.from_dict_list, factory=list) # observed_time_zone: str = field(default=None) # spam: bool = field(default=None) # time_observed_at: DateTime = datetime_attr # time_zone_offset: str = field(default=None) # Attributes that will only be used during init and then omitted temp_attrs = [ 'created_at_details', 'observed_on_string', 'observed_time_zone', 'time_zone_offset', ] # Convert observation timestamps prior to __attrs_init__ def __init__( self, # created_at_details: Dict = None, # observed_on_string: str = None, # observed_time_zone: str = None, # time_zone_offset: str = None, **kwargs, ): created_at_details = kwargs.pop('created_at_details', None) observed_on_string = kwargs.pop('observed_on_string', None) observed_time_zone = kwargs.pop('observed_time_zone', None) time_zone_offset = kwargs.pop('time_zone_offset', None) tz_offset = time_zone_offset tz_name = observed_time_zone created_date = (created_at_details or {}).get('date') if not isinstance(kwargs.get('created_at'), datetime) and created_date: kwargs['created_at'] = convert_observation_timestamp( created_date, tz_offset, tz_name) if not isinstance(kwargs.get('observed_on'), datetime) and observed_on_string: kwargs['observed_on'] = convert_observation_timestamp( observed_on_string, tz_offset, tz_name, ignoretz=True) if not kwargs.get('uri'): kwargs[ 'uri'] = f'{INAT_BASE_URL}/observations/{kwargs.get("id", "")}' self.__attrs_init__(**kwargs) # type: ignore @classmethod def from_id(cls, id: int): """Lookup and create a new Observation object from an ID""" from pyinaturalist.v1 import get_observation json = get_observation(id) return cls.from_json(json) @property def photo_url(self) -> Optional[str]: """Original size photo URL for first observation photo (if any)""" if not self.photos: return None return self.photos[0].original_url @property def thumbnail_url(self) -> Optional[str]: """Thumbnail size photo URL for first observation photo (if any)""" if not self.photos: return None return self.photos[0].thumbnail_url @property def _row(self) -> TableRow: return { 'ID': self.id, 'Taxon ID': self.taxon.id, 'Taxon': self.taxon.full_name, 'Observed on': self.observed_on, 'User': self.user.login, 'Location': self.place_guess or self.location, } @property def _str_attrs(self) -> List[str]: return ['id', 'taxon', 'observed_on', 'user', 'place_guess']
class Taxon(BaseModel): """:fa:`dove,style=fas` An iNaturalist taxon, based on the schema of `GET /taxa <https://api.inaturalist.org/v1/docs/#!/Taxa/get_taxa>`_. Can be constructed from either a full or partial JSON record. Examples of partial records include nested ``ancestors``, ``children``, and results from :py:func:`.get_taxa_autocomplete`. """ ancestry: str = field(default=None, doc='Slash-delimited string of ancestor IDs', repr=False) ancestor_ids: List[int] = field( factory=list, converter=ensure_list, # Handle arrays when converting from a dataframe 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') gbif_id: int = field(default=None, doc='GBIF taxon ID') iconic_taxon_id: int = field( default=0, doc='ID of the iconic taxon (e.g., general taxon "category")') iconic_taxon_name: str = field( default=None, doc='Name of the iconic taxon (e.g., general taxon "category")') is_active: bool = field( default=None, doc='Indicates if the taxon is active (and not renamed, moved, etc.)') listed_places: bool = field( default=None, doc='Indicates if there are listed places for this taxon') 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') parent_id: int = field(default=None, doc='Taxon ID of immediate ancestor') preferred_common_name: str = field( default='', doc='Common name for the preferred place, if any') preferred_establishment_means: str = field( default=None, doc= 'Establishment means for this taxon in the given preferred place (if any)' ) rank_level: int = field( default=None, doc= 'Number indicating rank level, for easier comparison between ranks (kingdom=higest)', ) ranges: bool = field(default=None, doc='Indicates if there is range data for this taxon') 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') vision: bool = field( default=None, doc='Indicates if this taxon is included in the computer vision model') wikipedia_summary: str = field(default=None, doc='Taxon summary from Wikipedia article') wikipedia_url: str = field(default=None, doc='URL to Wikipedia article for the taxon') # Lazy-loaded model objects ancestors: property = LazyProperty(BaseModel.from_json_list) children: property = LazyProperty(BaseModel.from_json_list) conservation_status: property = LazyProperty( ConservationStatus.from_json, type=ConservationStatus, doc='Conservation status of the taxon in a given location', ) conservation_statuses: property = LazyProperty( ConservationStatus.from_json_list, type=List[ConservationStatus], doc='Conservation statuses of the taxon in different locations', ) default_photo: property = LazyProperty(Photo.from_json, type=Photo, doc='Taxon default photo') establishment_means: property = LazyProperty( EstablishmentMeans.from_json, type=EstablishmentMeans, doc='Establishment means for a taxon in a given location', ) listed_taxa: property = LazyProperty( ListedTaxon.from_json_list, type=List[ListedTaxon], doc='Details about this taxon associated with a list', ) taxon_photos: property = LazyProperty( Photo.from_json_list, type=List[Photo], doc='All taxon photos shown on taxon info page') # Unused attributes # atlas_id: int = field(default=None) # flag_counts: Dict[str, int] = field(factory=dict) # {"unresolved": 1, "resolved": 2} # min_species_ancestry: str = field(default=None) # Used internally by iNaturalist for Elasticsearch aggregations # min_species_taxon_id: int = field(default=None) # partial: bool = field(default=None, repr=False) # photos_locked: bool = field(default=None) # universal_search_rank: int = field(default=None) def __attrs_post_init__(self): # Look up iconic taxon name, if only ID is provided if not self.iconic_taxon_name: self.iconic_taxon_name = ICONIC_TAXA.get(self.iconic_taxon_id, 'Unknown') # If default photo is missing, use iconic taxon icon if not self.default_photo: self.default_photo = self.icon # If only ancestor string (or objects) are provided, split into IDs if self.ancestry and not self.ancestor_ids: delimiter = ',' if ',' in self.ancestry else '/' self.ancestor_ids = [ int(x) for x in self.ancestry.split(delimiter) ] elif self.ancestors and not self.ancestor_ids: self.ancestor_ids = [t.name for t in self.ancestors] @classmethod def from_sorted_json_list(cls, value: JsonResponse) -> List['Taxon']: """Sort Taxon objects by rank then by name""" taxa = cls.from_json_list(value) taxa.sort(key=_get_rank_name_idx) return taxa @property def child_ids(self) -> List[int]: """Taxon IDs of direct children, sorted by rank then name""" return [t.id for t in self.children] @property def emoji(self) -> str: """Get an emoji representing the taxon""" for taxon_id in [self.id] + list(reversed(self.ancestor_ids)): if taxon_id in EMOJI: return EMOJI[taxon_id] return ICONIC_EMOJI.get(self.iconic_taxon_id, '❓') @property def full_name(self) -> str: """Taxon rank, scientific name, common name (if available), and emoji""" if not self.name and not self.rank: return 'unknown taxon' elif not self.name: return f'{self.rank.title()}: {self.id}' elif not self.rank: return self.name common_name = f' ({self.preferred_common_name})' if self.preferred_common_name else '' return f'{self.rank.title()}: {self.name}{common_name}' @property def icon(self) -> IconPhoto: return IconPhoto.from_iconic_taxon(self.iconic_taxon_name) @property def icon_url(self) -> str: """Iconic URL for the icon of the iconic taxon""" return str(self.icon.thumbnail_url) @property def gbif_url(self) -> str: """URL for the GBIF info page for this taxon""" return f'https://www.gbif.org/species/{self.gbif_id}' @property def parent(self) -> 'Taxon': """Immediate parent, if any""" return self.ancestors[-1] if self.ancestors else None @property def taxonomy(self) -> Dict[str, str]: """Ancestor + current taxon as a ``{rank: name}`` dict""" return {t.rank: t.name for t in self.ancestors + [self]} @property def url(self) -> str: """Info URL on iNaturalist.org""" return f'{INAT_BASE_URL}/taxa/{self.id}' # TODO: Probably remove this @classmethod def from_id(cls, id: int) -> 'Taxon': """Lookup and create a new Taxon object by ID""" from pyinaturalist.v1 import get_taxa_by_id r = get_taxa_by_id(id) return cls.from_json(r['results'][0]) def load_full_record(self): """Update this Taxon with full taxon info, including ancestors + children""" t = Taxon.from_id(self.id) for key in fields_dict(Taxon).keys(): key = key.lstrip( '_' ) # Use getters/setters for LazyProperty instead of temp attrs setattr(self, key, getattr(t, key)) @property def _row(self) -> TableRow: return { 'ID': self.id, 'Rank': self.rank, 'Scientific name': f'{self.emoji} {self.name}', 'Common name': self.preferred_common_name, } @property def _str_attrs(self) -> List[str]: return ['id', 'full_name']
@define_model_collection class TaxonCounts(BaseModelCollection): """:fa:`dove,style=fas` :fa:`list` A collection of taxa with an associated counts. Used with :v1:`GET /observations/species_counts <Observations/get_observations_species_counts>`. as well as :py:class:`.LifeList`. """ data: List[TaxonCount] = field(factory=list, converter=TaxonCount.from_json_list) # Since these use Taxon classmethods, they must be added after Taxon is defined Taxon.ancestors = LazyProperty( Taxon.from_sorted_json_list, name='ancestors', type=List[Taxon], doc='Ancestor taxa, from highest rank to lowest', ) Taxon.children = LazyProperty( Taxon.from_sorted_json_list, name='children', type=List[Taxon], doc='Child taxa, sorted by rank then name', ) def _get_rank_name_idx(taxon): """Sort index by rank and name (ascending)""" idx = RANKS.index(taxon.rank) if taxon.rank in RANKS else 0 return idx * -1, taxon.name
class Identification(BaseModel): """:fa:`fingerprint,style=fas` An observation identification, based on the schema of `GET /identifications <https://api.inaturalist.org/v1/docs/#!/Identifications/get_identifications>`_. """ body: str = field(default=None, doc='Comment text') 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') current_taxon: bool = field(default=None) disagreement: bool = field( default=None, doc='Indicates if this identification disagrees with previous ones') hidden: bool = field(default=None) own_observation: bool = field( default=None, doc='Indicates if the indentifier is also the observer') previous_observation_taxon_id: int = field( default=None, doc='Previous observation taxon ID') taxon_change: bool = field(default=None) # TODO: confirm type taxon_id: int = field(default=None, doc='Identification taxon ID') uuid: str = field(default=None, doc='Universally unique identifier') vision: bool = field( default=None, doc='Indicates if the taxon was selected from computer vision results') taxon: property = LazyProperty(Taxon.from_json, type=Taxon, doc='Identification taxon') user: property = LazyProperty(User.from_json, type=User, doc='User that added the indentification') @property def taxon_name(self) -> str: return self.taxon.full_name # Unused attributes # created_at_details: {} # spam: bool = field(default=None) # flags: List = field(factory=list) # moderator_actions: List = field(factory=list) # observation: {} @property def _row(self) -> TableRow: return { 'ID': self.id, 'Taxon ID': self.taxon.id, 'Taxon': f'{self.taxon.emoji} {self.taxon.full_name}', 'User': self.user.login, 'Category': self.category.title(), 'From CV': self.vision, } @property def _str_attrs(self) -> List[str]: return ['id', 'taxon_name', 'created_at']
def __init__(self): if TYPE_CHECKING: self.place = LazyProperty() self.updater = LazyProperty() self.user = LazyProperty()