示例#1
0
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']
示例#2
0
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}'
        )
示例#3
0
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']
示例#4
0
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']
示例#5
0
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']
示例#6
0
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']
示例#8
0
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']
示例#9
0
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']
示例#10
0
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']
示例#11
0
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']
示例#12
0
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']
示例#13
0
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']
示例#14
0
@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
示例#15
0
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']
示例#16
0
 def __init__(self):
     if TYPE_CHECKING:
         self.place = LazyProperty()
         self.updater = LazyProperty()
         self.user = LazyProperty()