From 65fa61e1c69c444a035c59e427c271d87cb17d52 Mon Sep 17 00:00:00 2001 From: Jordan Cook Date: Mon, 22 Jul 2024 17:44:55 -0500 Subject: [PATCH 1/2] Add Observation.taxon.ancestors based on identification data --- CONTRIBUTORS.md | 3 ++- HISTORY.md | 1 + pyinaturalist/models/observation.py | 15 +++++++++++++++ test/test_models.py | 18 ++++++++++++++++++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index bee450bb..96034cb2 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -4,16 +4,17 @@ questions, and other contributions that have helped to improve pyinaturalist: * [AE Hamrick](https://github.com/AEHamrick) * [abubelinha](https://github.com/abubelinha) +* [Arky](https://github.com/arky) * [Ben Armstrong](https://github.com/synrg) * [Dr. Tom August](https://github.com/AugustT) * [Eduardo Ramírez](https://github.com/eduramirezh) * [Felipe S Barros](https://github.com/FelipeSBarros) +* [jumear](https://github.com/jumear) * [Ken-ichi Ueda](https://github.com/kueda) * [Matt Muir](https://forum.inaturalist.org/u/muir) * [Nicolas Noé](https://github.com/niconoe) * [Patrick Leary](https://github.com/pleary) * [Peter Desmet](https://github.com/peterdesmet) -* [pisum](https://forum.inaturalist.org/u/pisum) * [Sam R](https://github.com/samtreesandbushes) * [SDJ Brown](https://github.com/sdjbrown) * [Stijn Van Hoey](https://github.com/stijnvanhoey) diff --git a/HISTORY.md b/HISTORY.md index c991b280..1d5bd4b8 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -10,6 +10,7 @@ * Add `root_id` filter to `taxon.make_tree()` to explicitly set the root taxon instead of determining it automatically * Fix `taxon.make_tree()` rank filtering to allow skipping any number of rank levels * `taxon.make_tree()` now returns copies of original taxon objects instead of modifying them in-place +* Update `Observation.taxon.ancestors` based on identification data, if available ### Rate limits, timeouts, and error handling * Increase default request timeout from 10 to 20 seconds diff --git a/pyinaturalist/models/observation.py b/pyinaturalist/models/observation.py index 82e528c9..5526a461 100644 --- a/pyinaturalist/models/observation.py +++ b/pyinaturalist/models/observation.py @@ -321,6 +321,21 @@ def __init__(self, **kwargs): kwargs['identifications_count'] = len(kwargs['identifications']) self.__attrs_init__(**kwargs) # type: ignore + def __attrs_post_init__(self): + """Add ancestor info to Observation.taxon based on identification data, if available.""" + if not self.taxon or not self.identifications: + return + + taxa_by_id = { + t.id: t + for t in chain.from_iterable( + [i.taxon.ancestors for i in self.identifications if i.taxon] + ) + } + self.taxon.ancestors = list( + filter(None, [taxa_by_id.get(i) for i in self.taxon.ancestor_ids]) + ) + @property def cumulative_ids(self) -> Tuple[int, int]: """Calculate the cumulative community ID score (agreements/total), as shown on the observation UI diff --git a/test/test_models.py b/test/test_models.py index 439e9ca4..36694449 100644 --- a/test/test_models.py +++ b/test/test_models.py @@ -600,6 +600,24 @@ def test_observation__cumulative_ids__most_agree(): assert obs.cumulative_ids == (2, 3) +def test_observation__add_ancestors(): + obs = Observation.from_json(j_observation_2) + assert [f'{t.rank} {t.name}' for t in obs.taxon.ancestors] == [ + 'kingdom Animalia', + 'phylum Arthropoda', + 'subphylum Hexapoda', + 'class Insecta', + 'subclass Pterygota', + 'order Lepidoptera', + 'superfamily Papilionoidea', + 'family Nymphalidae', + 'subfamily Danainae', + 'tribe Danaini', + 'subtribe Danaina', + 'genus Danaus', + ] + + def test_observations(): obs_list = Observations.from_json_list( [j_observation_1, j_observation_1, j_observation_2, j_observation_3_ofvs] From 67041c7bdca465095f0a8b20abe5456ba7b4b908 Mon Sep 17 00:00:00 2001 From: Jordan Cook Date: Mon, 22 Jul 2024 19:09:06 -0500 Subject: [PATCH 2/2] Handle in JSON (pre-init) instead of post-init to preserve lazy initialization --- pyinaturalist/models/observation.py | 26 +++++++++++--------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/pyinaturalist/models/observation.py b/pyinaturalist/models/observation.py index 5526a461..659ebee1 100644 --- a/pyinaturalist/models/observation.py +++ b/pyinaturalist/models/observation.py @@ -319,22 +319,18 @@ def __init__(self, **kwargs): # Set identifications_count if missing if kwargs.get('identifications') and not kwargs.get('identifications_count'): kwargs['identifications_count'] = len(kwargs['identifications']) - self.__attrs_init__(**kwargs) # type: ignore - - def __attrs_post_init__(self): - """Add ancestor info to Observation.taxon based on identification data, if available.""" - if not self.taxon or not self.identifications: - return - taxa_by_id = { - t.id: t - for t in chain.from_iterable( - [i.taxon.ancestors for i in self.identifications if i.taxon] - ) - } - self.taxon.ancestors = list( - filter(None, [taxa_by_id.get(i) for i in self.taxon.ancestor_ids]) - ) + # Add ancestor info to Observation.taxon based on identification data, if available + if kwargs.get('taxon') and (idents := kwargs.get('identifications')): + taxa_by_id = { + t['id']: t + for t in chain.from_iterable( + [i.get('taxon', {}).get('ancestors', []) for i in idents] + ) + } + ancestors = [taxa_by_id.get(i) for i in kwargs['taxon'].get('ancestor_ids')] + kwargs['taxon']['ancestors'] = list(filter(None, ancestors)) + self.__attrs_init__(**kwargs) # type: ignore @property def cumulative_ids(self) -> Tuple[int, int]: