Repository: niconoe/pyinaturalist
Pull request #200 (2021-07-05 20:45:36)

Add more taxon emoji
Because why not


Patch:
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index 91d0272..cebbd76 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -3,6 +3,7 @@ Thanks to the following individuals for providing pull requests, feedback, bug r
 questions, and other contributions that have helped to improve pyinaturalist:     
 
 * [AE Hamrick](https://github.com/AEHamrick)
+* [Ben Armstrong](https://github.com/synrg)
 * [Felipe S Barros](https://github.com/FelipeSBarros)
 * [Ken-ichi Ueda](https://github.com/kueda/)
 * [Matt Muir](https://forum.inaturalist.org/u/muir)
diff --git a/HISTORY.md b/HISTORY.md
index 46ed607..212aaec 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -1,6 +1,6 @@
 # History
 
-## 0.14 (2021-TBD)
+## 0.14 (2021-07-TBD)
 [See all Issues & PRs for 0.14](https://github.com/niconoe/pyinaturalist/milestone/5?closed=1)
 
 ### New Endpoints
@@ -61,8 +61,8 @@ Model features:
 ### Other Changes
 * Consolidated response formatting into a single `pprint()` function (instead of one per resource type)
 * Refactored and reorganized the following internal utility modules (see API docs for details):
-    * `api_docs`
     * `converters`
+    * `docs`
     * `formatters`
     * `request_params`
 * Added a default response timeout of 5 seconds
diff --git a/README.md b/README.md
index eb6105d..f951eda 100644
--- a/README.md
+++ b/README.md
@@ -251,6 +251,10 @@ about iNaturalist, the [iNaturalist Community Forum](https://forum.inaturalist.o
 place to start.
 
 ## Related Projects
+Other python projects related to iNaturalist:
+
+* [Dronefly](https://github.com/synrg/dronefly): A Discord bot with iNaturalist data access features,
+* [pyinaturalist-convert](https://github.com/JWCook/pyinaturalist-convert): Tools to convert observation data to and from multiple formats
 * [pyinaturalist-open-data](https://github.com/JWCook/pyinaturalist-open-data):  Tools for working with [iNaturalist open data](https://registry.opendata.aws/inaturalist-open-data/)
 * [pyinaturalist-notebook](https://github.com/JWCook/pyinaturalist-notebook): Jupyter notebook Docker image for pyinaturalist
-* [pyinaturalist-convert](https://github.com/JWCook/pyinaturalist-convert): (WIP) Tools to convert observation data to and from multiple formats
+  used by the iNaturalist Discord server.
diff --git a/docs/conf.py b/docs/conf.py
index f18f7a8..33587ed 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -3,7 +3,7 @@
 * ``forge`` is used to define and reuse documentation for API request parameters
 * ``apidoc`` is used to generate rst sources for **modules**
 * ``autosummary`` is used to generate rst sources for **packages and summaries**
-* ``automodapi`` + ``pyinaturalist.api_docs.model_docs`` are used to generate model
+* ``automodapi`` + ``pyinaturalist.docs.model_docs`` are used to generate model
   documentation based on ``attrs`` fields + metadata
 * ``intersphinx`` is used to insert links to other projects' docs
 * Jinja templates provide some additional customization:
@@ -34,7 +34,7 @@ except ImportError:
 sys.path.insert(0, '..')
 from pyinaturalist import __version__
 from pyinaturalist.constants import DOCS_DIR, PROJECT_DIR, EXAMPLES_DIR, SAMPLE_DATA_DIR
-from pyinaturalist.api_docs.model_docs import document_models
+from pyinaturalist.docs.model_docs import document_models
 
 # Relevant doc directories used in extension settings
 CSS_DIR = join(DOCS_DIR, '_static')
@@ -118,7 +118,7 @@ autodoc_typehints = 'none'
 # apidoc settings
 apidoc_module_dir = PACKAGE_DIR
 apidoc_output_dir = MODULE_DOCS_DIR
-apidoc_excluded_paths = ['api_docs/*', 'models/*', 'node_api.py', 'rest_api.py']
+apidoc_excluded_paths = ['docs/*', 'models/*', 'node_api.py', 'rest_api.py']
 apidoc_extra_args = ['--templatedir=_templates']
 apidoc_module_first = True
 apidoc_separate_modules = True
diff --git a/pyinaturalist/api_requests.py b/pyinaturalist/api_requests.py
index 9942fe3..5ee4528 100644
--- a/pyinaturalist/api_requests.py
+++ b/pyinaturalist/api_requests.py
@@ -9,7 +9,6 @@ from unittest.mock import Mock
 from requests import Response, Session
 
 import pyinaturalist
-from pyinaturalist.api_docs import copy_signature
 from pyinaturalist.constants import (
     MAX_DELAY,
     REQUESTS_PER_DAY,
@@ -19,6 +18,7 @@ from pyinaturalist.constants import (
     MultiInt,
     RequestParams,
 )
+from pyinaturalist.docs import copy_signature
 from pyinaturalist.request_params import prepare_request
 
 # Mock response content to return in dry-run mode
diff --git a/pyinaturalist/api_docs/__init__.py b/pyinaturalist/docs/__init__.py
similarity index 79%
rename from pyinaturalist/api_docs/__init__.py
rename to pyinaturalist/docs/__init__.py
index 6487913..7cc67a3 100644
--- a/pyinaturalist/api_docs/__init__.py
+++ b/pyinaturalist/docs/__init__.py
@@ -4,7 +4,8 @@
 * Dynamic docs: Function signatures + docstrings based on API request params
 * Static docs: Sphinx documentation on readthedocs.io
 """
-from pyinaturalist.api_docs.forge_utils import (
+from pyinaturalist.docs.emoji import EMOJI
+from pyinaturalist.docs.forge_utils import (
     copy_doc_signature,
     copy_docstrings,
     copy_signature,
diff --git a/pyinaturalist/docs/emoji.py b/pyinaturalist/docs/emoji.py
new file mode 100644
index 0000000..ccfaf98
--- /dev/null
+++ b/pyinaturalist/docs/emoji.py
@@ -0,0 +1,137 @@
+"""Extended list of emoji to represent taxa"""
+EMOJI = {
+    # Birds
+    3: '🐦',
+    574: '🦃',
+    879: '🐔',
+    71261: '🦅',
+    6888: '🦆',
+    1199: '🦚',
+    67569: '🦩',
+    2708: '🕊️',
+    6913: '🦢',
+    67564: '🐧',
+    19350: '🦉',
+    18874: '🦜',
+    # Amphibians + reptiles
+    20978: '🐸',
+    26036: '🦎',
+    26039: '🐊',
+    39532: '🐢',
+    85553: '🐍',
+    # Mammals
+    786045: '🐵',
+    43579: '🦍',
+    846280: '🦧',
+    47144: '🐶',
+    42048: '🐺',
+    42054: '🦊',
+    41660: '🦝',
+    41944: '🐱',
+    846273: '🐯',
+    43328: '🐴',
+    42158: '🦌',
+    42406: '🦬',
+    568826: '🐮',
+    848343: '🐷',
+    568847: '🐑',
+    42348: '🐐',
+    846209: '🐪',
+    925150: '🦙',
+    43691: '🐘',
+    43698: '🐹',
+    43094: '🐰',
+    45933: '🐿️',
+    43791: '🦫',
+    533971: '🦔',
+    40268: '🦇',
+    41636: '🐻',
+    848319: '🦘',
+    42981: '🐨',
+    53537: '🦥',
+    41770: '🦡',
+    526556: '🦦',
+    41871: '🦨',
+    846287: '🦡',
+    # Fish
+    47178: '🐟',
+    47233: '🐠',
+    47177: '🐡',
+    372843: '🦭',
+    152871: '🐋',
+    41479: '🐬',
+    47273: '🦈',
+    # Molluscs
+    47115: '🐌',
+    47459: '🐙',
+    127352: '🦑',
+    # Arthropods
+    85493: '🦀',
+    144114: '🦐',
+    311310: '🦞',
+    47119: '🕷️',
+    48894: '🦂',
+    172373: '🦂',
+    48900: '🦂',
+    372739: '🐛',
+    144128: '🐛',
+    47822: '🪰',
+    153429: '🦟',
+    81769: '🪳',
+    47651: '🦗',
+    47157: '🦋',
+    47201: '🐝',
+    47336: '🐜',
+    48511: '🐜',
+    47208: '🪲',
+    471714: '🐞',
+    # Plants
+    47126: '🌱',
+    136329: '🌲',
+    47903: '🌵',
+    47162: '🌾',
+    121943: '🌿',
+    70233: '🍀',
+    48866: '🌴',
+    544534: '🥥',
+    47605: '🌼',
+    605446: '🌻',
+    47690: '🌺',
+    48796: '🌺',
+    47329: '🌷',
+    47148: '🌹',
+    47727: '🍁',
+    53548: '🌳',
+    47853: '🌳',
+    151882: '🌳',
+    50998: '🌳',
+    47567: '🌳',
+    71434: '🌳',
+    47194: '🌳',
+    48699: '🥕',
+    47204: '🥦',
+    71291: '🍇',
+    48620: '🍈',
+    62910: '🍌',
+    50623: '🍊',
+    723302: '🍎',
+    49570: '🍍',
+    48874: '🥭',
+    47351: '🍒',
+    47733: '🫒',
+    55849: '🧄',
+    48516: '🍅',
+    48517: '🌶️',
+    62848: '🥑',
+    635417: '🌽',
+    55150: '🥝',
+    50299: '🍓',
+    634914: '🫐',
+    # Other
+    1: '🐾',
+    47491: '🪱',
+    47170: '🍄',
+    48222: '🟢',
+    67333: '🦠',
+    131236: '🦠',
+}
diff --git a/pyinaturalist/api_docs/forge_utils.py b/pyinaturalist/docs/forge_utils.py
similarity index 100%
rename from pyinaturalist/api_docs/forge_utils.py
rename to pyinaturalist/docs/forge_utils.py
diff --git a/pyinaturalist/api_docs/model_docs.py b/pyinaturalist/docs/model_docs.py
similarity index 100%
rename from pyinaturalist/api_docs/model_docs.py
rename to pyinaturalist/docs/model_docs.py
diff --git a/pyinaturalist/api_docs/templates.py b/pyinaturalist/docs/templates.py
similarity index 100%
rename from pyinaturalist/api_docs/templates.py
rename to pyinaturalist/docs/templates.py
diff --git a/pyinaturalist/models/comment.py b/pyinaturalist/models/comment.py
index 77e23e4..dc3a788 100644
--- a/pyinaturalist/models/comment.py
+++ b/pyinaturalist/models/comment.py
@@ -6,7 +6,7 @@ from pyinaturalist.models import BaseModel, LazyProperty, User, datetime_now_fie
 
 @define_model
 class Comment(BaseModel):
-    """An observation comment, based on the schema of comments
+    """📝 An observation comment, based on the schema of comments
     from `GET /observations <https://api.inaturalist.org/v1/docs/#!/Observations/get_observations>`_.
     """
 
diff --git a/pyinaturalist/models/controlled_term.py b/pyinaturalist/models/controlled_term.py
index a7860ce..51b3fe7 100644
--- a/pyinaturalist/models/controlled_term.py
+++ b/pyinaturalist/models/controlled_term.py
@@ -6,7 +6,7 @@ from pyinaturalist.models import BaseModel, LazyProperty, User, define_model, fi
 
 @define_model
 class Annotation(BaseModel):
-    """An annotation, meaning a **controlled term value** applied by a **user** to an **observation**.
+    """📝 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>`_.
     """
@@ -45,7 +45,7 @@ class Annotation(BaseModel):
 
 @define_model
 class ControlledTermValue(BaseModel):
-    """A controlled term **value**, based on the schema of
+    """📋 A controlled term **value**, based on the schema of
     `GET /controlled_terms <https://api.inaturalist.org/v1/docs/#!/Controlled_Terms/get_controlled_terms>`_.
     """
 
@@ -62,7 +62,7 @@ class ControlledTermValue(BaseModel):
 
 @define_model
 class ControlledTerm(BaseModel):
-    """A controlled term, based on the schema of
+    """📋 A controlled term, based on the schema of
     `GET /controlled_terms <https://api.inaturalist.org/v1/docs/#!/Controlled_Terms/get_controlled_terms>`_.
     """
 
diff --git a/pyinaturalist/models/identification.py b/pyinaturalist/models/identification.py
index 9b28172..802071d 100644
--- a/pyinaturalist/models/identification.py
+++ b/pyinaturalist/models/identification.py
@@ -14,7 +14,7 @@ from pyinaturalist.models import (
 
 @define_model
 class Identification(BaseModel):
-    """An observation identification, based on the schema of
+    """🔎 An observation identification, based on the schema of
     `GET /identifications <https://api.inaturalist.org/v1/docs/#!/Identifications/get_identifications>`_.
     """
 
diff --git a/pyinaturalist/models/life_list.py b/pyinaturalist/models/life_list.py
index 12f6fc2..cd14cb5 100644
--- a/pyinaturalist/models/life_list.py
+++ b/pyinaturalist/models/life_list.py
@@ -1,7 +1,7 @@
 from itertools import groupby
 from typing import List
 
-from pyinaturalist.constants import JsonResponse
+from pyinaturalist.constants import JsonResponse, TableRow
 from pyinaturalist.models import (
     Taxon,
     TaxonCount,
@@ -14,7 +14,7 @@ from pyinaturalist.models import (
 
 @define_model
 class LifeListTaxon(TaxonCount):
-    """A single taxon in a user's life list"""
+    """🐦 A single :py:class:`.Taxon` in a user's :py:class:`.LifeList`"""
 
     descendant_obs_count: int = field(default=0, doc='Number of observations of taxon children')
     direct_obs_count: int = field(
@@ -26,6 +26,15 @@ class LifeListTaxon(TaxonCount):
         """Indentation level corresponding to this item's rank level"""
         return int(((70 - self.rank_level) / 5))
 
+    @property
+    def row(self) -> TableRow:
+        return {
+            'ID': self.id,
+            'Rank': self.rank,
+            'Name': {self.name},
+            'Count': self.count,
+        }
+
     def __str__(self) -> str:
         padding = " " * self.indent_level
         return f'[{self.id:<8}] {padding} {self.rank.title()} {self.name}: {self.count}'
@@ -33,7 +42,7 @@ class LifeListTaxon(TaxonCount):
 
 @define_model_collection
 class LifeList(TaxonCounts):
-    """A user's life list, based on the schema of ``GET /observations/taxonomy``"""
+    """🐦✅📓 A user's life list, based on the schema of ``GET /observations/taxonomy``"""
 
     count_without_taxon: int = field(default=0)
     data: List[LifeListTaxon] = field(factory=list, converter=LifeListTaxon.from_json_list)  # type: ignore
diff --git a/pyinaturalist/models/observation.py b/pyinaturalist/models/observation.py
index b97c7dc..28fbe75 100644
--- a/pyinaturalist/models/observation.py
+++ b/pyinaturalist/models/observation.py
@@ -38,7 +38,7 @@ from pyinaturalist.v1 import get_observation
 
 @define(auto_attribs=False, init=False, field_transformer=add_lazy_attrs)
 class Observation(BaseModel):
-    """An observation, based the schema of
+    """👤🔎🐦 An observation, based the schema of
     `GET /observations <https://api.inaturalist.org/v1/docs/#!/Observations/get_observations>`_.
     """
 
@@ -224,7 +224,7 @@ class Observation(BaseModel):
 
 @define_model_collection
 class Observations(BaseModelCollection):
-    """A collection of observations"""
+    """👥🔍🐦 A collection of observations"""
 
     data: List[Observation] = field(factory=list, converter=Observation.from_json_list)
 
diff --git a/pyinaturalist/models/observation_field.py b/pyinaturalist/models/observation_field.py
index b203353..46ba36f 100644
--- a/pyinaturalist/models/observation_field.py
+++ b/pyinaturalist/models/observation_field.py
@@ -28,7 +28,7 @@ OFVValue = Union[date, datetime, float, int, str]
 
 @define_model
 class ObservationField(BaseModel):
-    """An observation field **definition**, based on the schema of
+    """📋 An observation field **definition**, based on the schema of
     `GET /observation_fields <https://www.inaturalist.org/pages/api+reference#get-observation_fields>`_.
     """
 
@@ -59,7 +59,7 @@ class ObservationField(BaseModel):
 
 @define_model
 class ObservationFieldValue(BaseModel):
-    """An observation field **value**, matching the schema of ``ofvs``
+    """📝 An observation field **value**, based on the schema of ``Observation.ofvs``
     from `GET /observations <https://api.inaturalist.org/v1/docs/#!/Observations/get_observations>`_.
     """
 
diff --git a/pyinaturalist/models/photo.py b/pyinaturalist/models/photo.py
index c502fc8..b2e4953 100644
--- a/pyinaturalist/models/photo.py
+++ b/pyinaturalist/models/photo.py
@@ -7,8 +7,10 @@ from pyinaturalist.models import BaseModel, define_model, field
 
 @define_model
 class Photo(BaseModel):
-    """An observation photo, based on the schema of photos from
-    `GET /observations <https://api.inaturalist.org/v1/docs/#!/Observations/get_observations>`_.
+    """📷 An observation photo, based on the schema of photos from:
+
+    * `GET /observations <https://api.inaturalist.org/v1/docs/#!/Observations/get_observations>`_
+    * `GET /taxa <https://api.inaturalist.org/v1/docs/#!/Taxa/get_taxa>`
     """
 
     attribution: str = field(default=None, doc='License attribution')
diff --git a/pyinaturalist/models/place.py b/pyinaturalist/models/place.py
index 75c76b7..ab169e6 100644
--- a/pyinaturalist/models/place.py
+++ b/pyinaturalist/models/place.py
@@ -20,7 +20,7 @@ def convert_optional_lat_long(obj: Union[Dict, List, None, str]):
 
 @define_model
 class Place(BaseModel):
-    """A curated or community-contributed place. Handles data from the following endpoints:
+    """📍 A curated or community-contributed place. Handles data from the following endpoints:
 
     * `GET /places/{id} <https://api.inaturalist.org/v1/docs/#!/Places/get_places_id>`_
     * `GET /places/nearby <https://api.inaturalist.org/v1/docs/#!/Places/get_places_nearby>`_
diff --git a/pyinaturalist/models/project.py b/pyinaturalist/models/project.py
index 5aa77d6..4342bfa 100644
--- a/pyinaturalist/models/project.py
+++ b/pyinaturalist/models/project.py
@@ -24,7 +24,7 @@ from pyinaturalist.models import (
 
 @define_model
 class ProjectObservation(BaseModel):
-    """Metadata about an observation that has been added to a project"""
+    """👤🔎🐦 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}
@@ -37,7 +37,7 @@ class ProjectObservation(BaseModel):
 
 @define_model
 class ProjectObservationField(ObservationField):
-    """An :py:class:`.ObservationField` with additional project-specific information"""
+    """📋 An :py:class:`.ObservationField` with additional project-specific information"""
 
     project_observation_field_id: int = field(default=None)
     position: int = field(default=None)
@@ -55,7 +55,7 @@ class ProjectObservationField(ObservationField):
 
 @define_model
 class ProjectUser(User):
-    """A :py:class:`.User` with additional project-specific information"""
+    """👤 A :py:class:`.User` with additional project-specific information"""
 
     project_id: int = field(default=None)
     project_user_id: int = field(default=None)
@@ -73,7 +73,7 @@ class ProjectUser(User):
 
 @define_model
 class Project(BaseModel):
-    """A dataclass containing information about a project, matching the schema of
+    """👥 An iNaturalist project, based on the schema of
     `GET /projects <https://api.inaturalist.org/v1/docs/#!/Projects/get_projects>`_.
     """
 
diff --git a/pyinaturalist/models/search.py b/pyinaturalist/models/search.py
index 1b286d4..22f8bc4 100644
--- a/pyinaturalist/models/search.py
+++ b/pyinaturalist/models/search.py
@@ -10,7 +10,7 @@ SearchResultRecord = Union[Place, Project, Taxon, User]
 
 @define_model
 class SearchResult(BaseModel):
-    """A dataclass for search results, matching the schema of
+    """🔍 A search result of any type, based on the schema of
     `GET /search <https://api.inaturalist.org/v1/docs/#!/Search/get_search>`_.
     """
 
diff --git a/pyinaturalist/models/taxon.py b/pyinaturalist/models/taxon.py
index 1c40622..3ce7a70 100644
--- a/pyinaturalist/models/taxon.py
+++ b/pyinaturalist/models/taxon.py
@@ -13,6 +13,7 @@ from pyinaturalist.constants import (
     JsonResponse,
     TableRow,
 )
+from pyinaturalist.docs import EMOJI
 from pyinaturalist.models import (
     BaseModel,
     BaseModelCollection,
@@ -33,10 +34,10 @@ from pyinaturalist.v1 import get_taxa_by_id
 # TODO: Include codes from other sources? Currently only including IUCN codes.
 @define_model
 class ConservationStatus(BaseModel):
-    """The conservation status of a taxon in a given location, based on the schema of:
+    """‼️ The conservation status of a taxon in a given location, based on the schema of:
 
-    * Taxon.conservation_status from `GET /taxa <https://api.inaturalist.org/v1/docs/#!/Taxa/get_taxa>`_
-    * Observation.taxon.conservation_statused from `GET /observations <https://api.inaturalist.org/v1/docs/#!/Observations/get_observations>`_
+    * ``Taxon.conservation_status`` from `GET /taxa <https://api.inaturalist.org/v1/docs/#!/Taxa/get_taxa>`_
+    * ``Observation.taxon.conservation_statuses`` from `GET /observations <https://api.inaturalist.org/v1/docs/#!/Observations/get_observations>`_
     """
 
     authority: str = field(default=None, doc='Data source for conservation status')
@@ -74,7 +75,7 @@ class ConservationStatus(BaseModel):
 
 @define_model
 class EstablishmentMeans(BaseModel):
-    """The establishment means for a taxon in a given location"""
+    """‼️ The establishment means for a taxon in a given location"""
 
     establishment_means: str = field(
         default=None, options=ESTABLISTMENT_MEANS, doc='Establishment means description'
@@ -89,7 +90,7 @@ class EstablishmentMeans(BaseModel):
 
 @define_model
 class Taxon(BaseModel):
-    """An iNaturalist taxon, based on the schema of
+    """🐦 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
@@ -202,28 +203,22 @@ class Taxon(BaseModel):
 
     @property
     def emoji(self) -> str:
-        """Get an emoji representing the iconic taxon"""
+        """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, and common name (if available).
-
-        Example:
-
-            >>> taxon.full_name
-            'Genus: Physcia (Rosette Lichens)'
-
-        """
+        """Taxon rank, scientific name, common name (if available), and emoji"""
         if not self.name and not self.rank:
             return 'unknown taxon'
         if not self.name:
-            name = str(self.id)
-        else:
-            common_name = self.preferred_common_name
-            name = self.name + (f' ({common_name})' if common_name else '')
+            return f'{self.rank.title()}: {self.id}'
 
-        return f'{self.rank.title()}: {name}'
+        common_name = f' ({self.preferred_common_name})' if self.preferred_common_name else ''
+        return f'{self.emoji} {self.rank.title()}: {self.name}{common_name}'
 
     @property
     def icon_url(self) -> str:
@@ -268,7 +263,9 @@ class Taxon(BaseModel):
 
 @define_model
 class TaxonCount(Taxon):
-    """A taxon with an associated count, used in a :py:class:`.TaxonCounts` collection"""
+    """🐦#️⃣ A :py:class:`.Taxon` with an associated count, used in a :py:class:`.TaxonCounts`
+    collection
+    """
 
     count: int = field(default=0, doc='Number of observations of this taxon')
 
@@ -286,7 +283,7 @@ class TaxonCount(Taxon):
         return {
             'ID': self.id,
             'Rank': self.rank,
-            'Name': self.name,
+            'Name': f'{self.emoji} {self.name}',
             'Count': self.count,
         }
 
@@ -296,7 +293,7 @@ class TaxonCount(Taxon):
 
 @define_model_collection
 class TaxonCounts(BaseModelCollection):
-    """A collection of taxa with an associated counts. Used with
+    """🐦#️⃣📓 A collection of taxa with an associated counts. Used with
     `GET /observations/species_counts <https://api.inaturalist.org/v1/docs/#!/Observations/get_observations_species_counts>`_.
     as well as :py:class:`.LifeList`.
     """
diff --git a/pyinaturalist/models/user.py b/pyinaturalist/models/user.py
index 9e93d98..60e33d3 100644
--- a/pyinaturalist/models/user.py
+++ b/pyinaturalist/models/user.py
@@ -7,7 +7,7 @@ from pyinaturalist.models import BaseModel, datetime_now_field, define_model, fi
 
 @define_model
 class User(BaseModel):
-    """A dataclass containing information about an user, matching the schema of
+    """👤 An iNaturalist user, based on the schema of
     `GET /users/{id} <https://api.inaturalist.org/v1/docs/#!/Users/get_users_id>`_.
     """
 
diff --git a/pyinaturalist/v0/observation_fields.py b/pyinaturalist/v0/observation_fields.py
index d2dc738..ead5e47 100644
--- a/pyinaturalist/v0/observation_fields.py
+++ b/pyinaturalist/v0/observation_fields.py
@@ -1,10 +1,10 @@
 from typing import Any
 
-from pyinaturalist.api_docs import document_request_params
-from pyinaturalist.api_docs import templates as docs
 from pyinaturalist.api_requests import get, put
 from pyinaturalist.constants import API_V0_BASE_URL, JsonResponse
 from pyinaturalist.converters import convert_all_timestamps
+from pyinaturalist.docs import document_request_params
+from pyinaturalist.docs import templates as docs
 from pyinaturalist.pagination import add_paginate_all
 
 
diff --git a/pyinaturalist/v0/observations.py b/pyinaturalist/v0/observations.py
index 7873d88..455976a 100644
--- a/pyinaturalist/v0/observations.py
+++ b/pyinaturalist/v0/observations.py
@@ -1,8 +1,6 @@
 from logging import getLogger
 from typing import List, Union
 
-from pyinaturalist.api_docs import document_request_params
-from pyinaturalist.api_docs import templates as docs
 from pyinaturalist.api_requests import delete, get, post, put
 from pyinaturalist.constants import (
     API_V0_BASE_URL,
@@ -17,6 +15,8 @@ from pyinaturalist.converters import (
     ensure_file_obj,
     ensure_list,
 )
+from pyinaturalist.docs import document_request_params
+from pyinaturalist.docs import templates as docs
 from pyinaturalist.exceptions import ObservationNotFound
 from pyinaturalist.pagination import add_paginate_all
 from pyinaturalist.request_params import convert_observation_fields, validate_multiple_choice_param
diff --git a/pyinaturalist/v1/identifications.py b/pyinaturalist/v1/identifications.py
index 73cc898..e3249f1 100644
--- a/pyinaturalist/v1/identifications.py
+++ b/pyinaturalist/v1/identifications.py
@@ -1,7 +1,7 @@
-from pyinaturalist.api_docs import document_request_params
-from pyinaturalist.api_docs import templates as docs
 from pyinaturalist.constants import JsonResponse, MultiInt
 from pyinaturalist.converters import convert_all_timestamps
+from pyinaturalist.docs import document_request_params
+from pyinaturalist.docs import templates as docs
 from pyinaturalist.pagination import add_paginate_all
 from pyinaturalist.request_params import convert_rank_range
 from pyinaturalist.v1 import get_v1
diff --git a/pyinaturalist/v1/observations.py b/pyinaturalist/v1/observations.py
index 0be1094..a88e9a8 100644
--- a/pyinaturalist/v1/observations.py
+++ b/pyinaturalist/v1/observations.py
@@ -1,5 +1,3 @@
-from pyinaturalist.api_docs import document_request_params
-from pyinaturalist.api_docs import templates as docs
 from pyinaturalist.constants import (
     NODE_OBS_ORDER_BY_PROPERTIES,
     HistogramResponse,
@@ -12,6 +10,8 @@ from pyinaturalist.converters import (
     convert_histogram,
     convert_observation_timestamps,
 )
+from pyinaturalist.docs import document_request_params
+from pyinaturalist.docs import templates as docs
 from pyinaturalist.exceptions import ObservationNotFound
 from pyinaturalist.pagination import add_paginate_all
 from pyinaturalist.request_params import validate_multiple_choice_param
diff --git a/pyinaturalist/v1/places.py b/pyinaturalist/v1/places.py
index 19bab8b..13e52a3 100644
--- a/pyinaturalist/v1/places.py
+++ b/pyinaturalist/v1/places.py
@@ -1,7 +1,7 @@
-from pyinaturalist.api_docs import document_request_params
-from pyinaturalist.api_docs import templates as docs
 from pyinaturalist.constants import JsonResponse, MultiInt
 from pyinaturalist.converters import convert_all_coordinates, convert_all_place_coordinates
+from pyinaturalist.docs import document_request_params
+from pyinaturalist.docs import templates as docs
 from pyinaturalist.pagination import add_paginate_all
 from pyinaturalist.v1 import get_v1
 
diff --git a/pyinaturalist/v1/projects.py b/pyinaturalist/v1/projects.py
index 71ccdcf..ed3b4c1 100644
--- a/pyinaturalist/v1/projects.py
+++ b/pyinaturalist/v1/projects.py
@@ -1,7 +1,7 @@
-from pyinaturalist.api_docs import document_request_params
-from pyinaturalist.api_docs import templates as docs
 from pyinaturalist.constants import PROJECT_ORDER_BY_PROPERTIES, JsonResponse, MultiInt
 from pyinaturalist.converters import convert_all_coordinates, convert_all_timestamps
+from pyinaturalist.docs import document_request_params
+from pyinaturalist.docs import templates as docs
 from pyinaturalist.pagination import add_paginate_all
 from pyinaturalist.request_params import validate_multiple_choice_param
 from pyinaturalist.v1 import get_v1
diff --git a/pyinaturalist/v1/search.py b/pyinaturalist/v1/search.py
index c5cb6ef..25dd862 100644
--- a/pyinaturalist/v1/search.py
+++ b/pyinaturalist/v1/search.py
@@ -1,7 +1,7 @@
-from pyinaturalist.api_docs import document_request_params
-from pyinaturalist.api_docs import templates as docs
 from pyinaturalist.constants import JsonResponse
 from pyinaturalist.converters import convert_all_coordinates, convert_all_timestamps
+from pyinaturalist.docs import document_request_params
+from pyinaturalist.docs import templates as docs
 from pyinaturalist.v1 import get_v1
 
 
diff --git a/pyinaturalist/v1/taxa.py b/pyinaturalist/v1/taxa.py
index a6de815..1f102db 100644
--- a/pyinaturalist/v1/taxa.py
+++ b/pyinaturalist/v1/taxa.py
@@ -1,7 +1,7 @@
-from pyinaturalist.api_docs import document_request_params
-from pyinaturalist.api_docs import templates as docs
 from pyinaturalist.constants import JsonResponse, MultiInt
 from pyinaturalist.converters import convert_all_timestamps
+from pyinaturalist.docs import document_request_params
+from pyinaturalist.docs import templates as docs
 from pyinaturalist.pagination import add_paginate_all
 from pyinaturalist.request_params import convert_rank_range
 from pyinaturalist.v1 import get_v1
diff --git a/pyinaturalist/v1/users.py b/pyinaturalist/v1/users.py
index 0a434f0..67ce82e 100644
--- a/pyinaturalist/v1/users.py
+++ b/pyinaturalist/v1/users.py
@@ -1,9 +1,9 @@
 from logging import getLogger
 
-from pyinaturalist.api_docs import document_request_params
-from pyinaturalist.api_docs import templates as docs
 from pyinaturalist.constants import JsonResponse
 from pyinaturalist.converters import convert_all_timestamps, convert_generic_timestamps
+from pyinaturalist.docs import document_request_params
+from pyinaturalist.docs import templates as docs
 from pyinaturalist.v1 import get_v1
 
 logger = getLogger(__name__)
diff --git a/pyproject.toml b/pyproject.toml
index 5d3eea6..6b491a6 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -83,7 +83,7 @@ directory = 'test-reports'
 [tool.coverage.run]
 branch = true
 source = ['pyinaturalist']
-omit = ['pyinaturalist/api_docs/model_docs.py']
+omit = ['pyinaturalist/docs/model_docs.py']
 
 [tool.isort]
 profile = 'black'


Test patch:
diff --git a/test/test_forge_utils.py b/test/test_forge_utils.py
index a70d894..3a1fe52 100644
--- a/test/test_forge_utils.py
+++ b/test/test_forge_utils.py
@@ -1,6 +1,6 @@
-from pyinaturalist.api_docs import copy_docstrings
+from pyinaturalist.docs import copy_docstrings
 
-# from pyinaturalist.api_docs import copy_signatures, document_request_params
+# from pyinaturalist.docs import copy_signatures, document_request_params
 
 
 def test_document_request_params():
diff --git a/test/test_formatters.py b/test/test_formatters.py
index fa8e714..a9bd2e2 100644
--- a/test/test_formatters.py
+++ b/test/test_formatters.py
@@ -72,7 +72,7 @@ def test_format_identifications(input):
 @pytest.mark.parametrize('input', get_variations(j_observation_1))
 def test_format_observation(input):
     expected_str = (
-        '[16227955] Species: Lixus bardanae observed on 2018-09-05 14:06:00+01:00 '
+        '[16227955] 🪲 Species: Lixus bardanae observed on 2018-09-05 14:06:00+01:00 '
         'by niconoe at 54 rue des Badauds'
     )
     assert format_observations(input) == expected_str
@@ -104,7 +104,7 @@ def test_format_places__nearby():
 
 def test_format_search_results():
     expected_str = (
-        '[Taxon] [47792] Order: Odonata (Dragonflies and Damselflies)\n'
+        '[Taxon] [47792] 🐛 Order: Odonata (Dragonflies and Damselflies)\n'
         '[Place] [113562] Odonates of Peninsular India and Sri Lanka\n'
         '[Project] [9978] Ohio Dragonfly Survey  (Ohio Odonata Survey)\n'
         '[User] [113886] odonatanb (Gilles Belliveau)'
@@ -114,19 +114,19 @@ def test_format_search_results():
 
 @pytest.mark.parametrize('input', get_variations(j_species_count_1))
 def test_format_species_counts(input):
-    expected_str = '[48484] Species: Harmonia axyridis (Asian Lady Beetle): 31'
+    expected_str = '[48484] 🐞 Species: Harmonia axyridis (Asian Lady Beetle): 31'
     assert format_species_counts(input) == expected_str
 
 
 @pytest.mark.parametrize('input', get_variations(j_taxon_1))
 def test_format_taxa__with_common_name(input):
-    expected_str = '[70118] Species: Nicrophorus vespilloides (Lesser Vespillo Burying Beetle)'
+    expected_str = '[70118] 🪲 Species: Nicrophorus vespilloides (Lesser Vespillo Burying Beetle)'
     assert format_taxa(input) == expected_str
 
 
 @pytest.mark.parametrize('input', get_variations(j_taxon_3_no_common_name))
 def test_format_taxon__without_common_name(input):
-    assert format_taxa(input) == '[124162] Species: Temnostoma vespiforme'
+    assert format_taxa(input) == '[124162] 🪰 Species: Temnostoma vespiforme'
 
 
 @pytest.mark.parametrize('input', get_variations(j_user_2_partial))
diff --git a/test/test_models.py b/test/test_models.py
index 5cd5093..014df00 100644
--- a/test/test_models.py
+++ b/test/test_models.py
@@ -136,7 +136,7 @@ def test_identification__empty():
 def test_identification__str():
     identification = ID.from_json(j_identification_3)
     assert str(identification) == (
-        '[126501311] Species: Danaus plexippus (Monarch) (improving) added on '
+        '[126501311] 🦋 Species: Danaus plexippus (Monarch) (improving) added on '
         '2020-08-27 13:00:51-05:00 by samroom'
     )