diff --git a/CHANGELOG.md b/CHANGELOG.md index 425f3aa..613266d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) (none) +## \[v0.3.4\] - 2024-06-29 + +### Added + +- Documentation pages for `alert` and `types_`. + +### Changed + +- Updated docs dependencies. This helped fix a bug that was preventing some documentation from building. +- Modernized some type hints to (e.g.,) use ` | ` instead of `typing.Optional`. +- Moved usage examples into the respective class docstrings. +- Cleaned up some documentation verbiage and Sphinx directives. + ## \[v0.3.3\] - 2024-06-28 ### Changed diff --git a/docs/source/api-reference/alert.rst b/docs/source/api-reference/alert.rst new file mode 100644 index 0000000..7939c8d --- /dev/null +++ b/docs/source/api-reference/alert.rst @@ -0,0 +1,7 @@ +pittgoogle.alert +================ + +.. automodule:: pittgoogle.alert + :members: + :private-members: + :member-order: bysource diff --git a/docs/source/api-reference/index.rst b/docs/source/api-reference/index.rst new file mode 100644 index 0000000..32b8fd3 --- /dev/null +++ b/docs/source/api-reference/index.rst @@ -0,0 +1,15 @@ +pittgoogle +========== + +.. These are from the ___init__.py file. Would be nice to find a way to pull them in automatically. + +.. autosummary:: + + pittgoogle.Alert + pittgoogle.Auth + pittgoogle.Consumer + pittgoogle.ProjectIds + pittgoogle.Schemas + pittgoogle.Subscription + pittgoogle.Table + pittgoogle.Topic diff --git a/docs/source/api-reference/types_.rst b/docs/source/api-reference/types_.rst new file mode 100644 index 0000000..32a97c1 --- /dev/null +++ b/docs/source/api-reference/types_.rst @@ -0,0 +1,7 @@ +pittgoogle.types_ +================= + +.. automodule:: pittgoogle.types_ + :members: + :private-members: + :member-order: bysource diff --git a/docs/source/conf.py b/docs/source/conf.py index 3961465..29daf1b 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -37,15 +37,16 @@ # ones. extensions = [ "myst_parser", - "sphinx.ext.autosectionlabel", "sphinx.ext.autodoc", "sphinx.ext.coverage", - "sphinx.ext.mathjax", "sphinx.ext.viewcode", "sphinx.ext.autosummary", "sphinx.ext.napoleon", - "sphinx_autodoc_typehints", "sphinx_copybutton", + # [FIXME] do we really need these? + # "sphinx_autodoc_typehints", # causes error can't resolve forward reference + "sphinx.ext.autosectionlabel", + "sphinx.ext.mathjax", ] set_type_checking_flag = True # set typing.TYPE_CHECKING = True diff --git a/docs/source/index.rst b/docs/source/index.rst index 872d057..388e96d 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -27,9 +27,12 @@ If you run into trouble, please :caption: API Reference :maxdepth: 1 + api-reference/index + api-reference/alert api-reference/auth api-reference/bigquery api-reference/exceptions api-reference/pubsub api-reference/registry + api-reference/types_ api-reference/utils diff --git a/docs/source/main/for-developers/manage-dependencies-poetry.md b/docs/source/main/for-developers/manage-dependencies-poetry.md index 2d0b7da..49c7437 100644 --- a/docs/source/main/for-developers/manage-dependencies-poetry.md +++ b/docs/source/main/for-developers/manage-dependencies-poetry.md @@ -6,6 +6,7 @@ Poetry was implemented in this repo in [pull #7](https://github.com/mwvgroup/pit ## Setup your environment Create a new conda environment for poetry and install it ([Poetry installation](https://python-poetry.org/docs/#installation)). +If you already did this, just activate the environment. ```bash conda create --name poetry-py311 python=3.11 diff --git a/pittgoogle/alert.py b/pittgoogle/alert.py index a664130..68dcfb1 100644 --- a/pittgoogle/alert.py +++ b/pittgoogle/alert.py @@ -1,12 +1,12 @@ # -*- coding: UTF-8 -*- -"""Classes to facilitate working with astronomical alerts.""" +"""Classes for working with astronomical alerts.""" import base64 import importlib.resources import io import logging from datetime import datetime from pathlib import Path -from typing import TYPE_CHECKING, Any, Dict, Mapping, Optional, Union +from typing import TYPE_CHECKING, Any, Mapping, Union import fastavro from attrs import define, field @@ -24,70 +24,63 @@ @define(kw_only=True) class Alert: - """Pitt-Google container for an astronomical alert. + """Container for an astronomical alert. Instances of this class are returned by other calls like :meth:`pittgoogle.Subscription.pull_batch`, so it is often not necessary to instantiate this directly. - In cases where you do want to create an `Alert` directly, use one of the `from_*` methods like `pittgoogle.Alert.from_dict`. + In cases where you do want to create an `Alert` directly, use one of the `from_*` methods like + :meth:`pittgoogle.Alert.from_dict`. All parameters are keyword only. - Parameters - ---------- - bytes : `bytes` (optional) - The message payload, as returned by Pub/Sub. It may be Avro or JSON serialized depending - on the topic. - dict : `dict` (optional) - The message payload as a dictionary. - metadata : `dict` (optional) - The message metadata. - msg : `google.cloud.pubsub_v1.types.PubsubMessage` (optional) - The Pub/Sub message object, documented at - ``__. - schema_name : `str` - One of (case insensitive): - - ztf - - ztf.lite - - elasticc.v0_9_1.alert - - elasticc.v0_9_1.brokerClassification - Schema name of the alert. Used for unpacking. If not provided, some properties of the - `Alert` may not be available. + Args: + bytes (bytes, optional): + The message payload, as returned by Pub/Sub. It may be Avro or JSON serialized depending + on the topic. + dict (dict, optional): + The message payload as a dictionary. + metadata (dict, optional): + The message metadata. + msg (google.cloud.pubsub_v1.types.PubsubMessage, optional): + The Pub/Sub message object, documented at + ``__. + schema_name (str): + Schema name of the alert. Used for unpacking. If not provided, some properties of the + `Alert` may not be available. See :meth:`pittgoogle.Schemas.names` for a list of options. """ - msg: Optional[ - Union["google.cloud.pubsub_v1.types.PubsubMessage", types_.PubsubMessageLike] - ] = field(default=None) - """Incoming Pub/Sub message object.""" - _attributes: Optional[Mapping[str, str]] = field(default=None) - _dict: Optional[Dict] = field(default=None) - _dataframe: Optional["pd.DataFrame"] = field(default=None) - schema_name: Optional[str] = field(default=None) - _schema: Optional[types_.Schema] = field(default=None, init=False) - path: Optional[Path] = field(default=None) + # Use "Union" because " | " is throwing an error when combined with forward references. + msg: Union["google.cloud.pubsub_v1.types.PubsubMessage", types_.PubsubMessageLike, None] = ( + field(default=None) + ) + _attributes: Mapping[str, str] | None = field(default=None) + _dict: Mapping | None = field(default=None) + _dataframe: Union["pd.DataFrame", None] = field(default=None) + schema_name: str | None = field(default=None) + _schema: types_.Schema | None = field(default=None, init=False) + path: Path | None = field(default=None) # ---- class methods ---- # @classmethod - def from_cloud_run(cls, envelope: Dict, schema_name: Optional[str] = None) -> "Alert": + def from_cloud_run(cls, envelope: Mapping, schema_name: str | None = None) -> "Alert": """Create an `Alert` from an HTTP request envelope containing a Pub/Sub message, as received by a Cloud Run module. - Parameters - ---------- - envelope : dict - The HTTP request envelope containing the Pub/Sub message. - schema_name : str (optional) - The name of the schema to use. Defaults to None. + Args: + envelope (dict): + The HTTP request envelope containing the Pub/Sub message. + schema_name (str, optional): + The name of the schema to use. Defaults to None. - Returns - ------- - Alert : An instance of the `Alert` class. + Returns: + Alert: + An instance of the `Alert` class. - Raises - ------ - BadRequest : If the Pub/Sub message is invalid or missing. + Raises: + BadRequest: + If the Pub/Sub message is invalid or missing. - Example - ------- - Code for a Cloud Run module that uses this method to open a ZTF alert: + Example: + Code for a Cloud Run module that uses this method to open a ZTF alert: .. code-block:: python @@ -144,65 +137,63 @@ def index(): @classmethod def from_dict( cls, - payload: Dict, - attributes: Optional[Mapping[str, str]] = None, - schema_name: Optional[str] = None, + payload: Mapping, + attributes: Mapping[str, str] | None = None, + schema_name: str | None = None, ) -> "Alert": """Create an `Alert` object from the given `payload` dictionary. - Parameters - ---------- - payload : dict - The dictionary containing the data for the `Alert` object. - attributes : dict-like (optional) - Additional attributes for the `Alert` object. Defaults to None. - schema_name : str (optional) - The name of the schema. Defaults to None. - - Returns - ------- - Alert: An instance of the `Alert` class. + Args: + payload (dict): + The dictionary containing the data for the `Alert` object. + attributes (Mapping[str, str], None): + Additional attributes for the `Alert` object. Defaults to None. + schema_name (str, None): + The name of the schema. Defaults to None. + + Returns: + Alert: + An instance of the `Alert` class. """ return cls(dict=payload, attributes=attributes, schema_name=schema_name) @classmethod - def from_msg(cls, msg, schema_name: Optional[str] = None) -> "Alert": - # [FIXME] This type hint is causing an error when building docs. - # Warning, treated as error: - # Cannot resolve forward reference in type annotations of "pittgoogle.alert.Alert.from_msg": - # name 'google' is not defined - # cls, msg: "google.cloud.pubsub_v1.types.PubsubMessage", schema_name: Optional[str] = None - """ - Create an `Alert` object from a `google.cloud.pubsub_v1.types.PubsubMessage`. - - Parameters - ---------- - msg : `google.cloud.pubsub_v1.types.PubsubMessage` - The PubsubMessage object to create the Alert from. - schema_name : str (optional) - The name of the schema to use for the Alert. Defaults to None. - - Returns - ------- - Alert : The created `Alert` object. + def from_msg( + cls, msg: "google.cloud.pubsub_v1.types.PubsubMessage", schema_name: str | None = None + ) -> "Alert": + """Create an `Alert` object from a `google.cloud.pubsub_v1.types.PubsubMessage`. + + Args: + msg (google.cloud.pubsub_v1.types.PubsubMessage): + The PubsubMessage object to create the Alert from. + schema_name (str, optional): + The name of the schema to use for the Alert. Defaults to None. + + Returns: + Alert: + The created `Alert` object. """ return cls(msg=msg, schema_name=schema_name) @classmethod - def from_path(cls, path: Union[str, Path], schema_name: Optional[str] = None) -> "Alert": - """Create an `Alert` object from the file at `path`. - - Parameters - ---------- - path : str or Path - The path to the file containing the alert data. - schema_name : str, optional - The name of the schema to use for the alert, by default None. - - Returns - ------- - Alert - An instance of the `Alert` class. + def from_path(cls, path: str | Path, schema_name: str | None = None) -> "Alert": + """Creates an `Alert` object from the file at the specified `path`. + + Args: + path (str or Path): + The path to the file containing the alert data. + schema_name (str, optional): + The name of the schema to use for the alert. Defaults to None. + + Returns: + Alert: + An instance of the `Alert` class. + + Raises: + FileNotFoundError: + If the file at the specified `path` does not exist. + IOError: + If there is an error reading the file. """ with open(path, "rb") as f: bytes_ = f.read() @@ -212,32 +203,50 @@ def from_path(cls, path: Union[str, Path], schema_name: Optional[str] = None) -> # ---- properties ---- # @property - def attributes(self) -> Dict: - """Custom metadata for the message. Pub/Sub handles this as a dict-like called "attributes". - - If this was not set when the `Alert` was instantiated, a new dictionary will be created using - the `attributes` field in :attr:`pittgoogle.Alert.msg` the first time it is requested. - Update this dictionary as desired. - Updates will not affect the original `msg`. - When publishing the alert using :attr:`pittgoogle.Topic.publish`, this dictionary will be - sent as the Pub/Sub message attributes. + def attributes(self) -> Mapping: + """Return the alert's custom metadata. + + If this was not provided (typical case), this attribute will contain a copy of + the incoming :attr:`Alert.msg.attributes`. + + You may update this dictionary as desired. If you publish this alert using + :attr:`pittgoogle.Topic.publish`, this dictionary will be sent as the outgoing + message's Pub/Sub attributes. """ if self._attributes is None: self._attributes = dict(self.msg.attributes) return self._attributes @property - def dict(self) -> Dict: - """Alert data as a dictionary. Created from `self.msg.data`, if needed. - - Raises - ------ - :class:`pittgoogle.exceptions.OpenAlertError` - If unable to deserialize the alert bytes. + def dict(self) -> Mapping: + """Return the alert data as a dictionary. + + If this was not provided (typical case), this attribute will contain the deserialized + alert bytes stored in the incoming :attr:`Alert.msg.data` as a dictionary. + + You may update this dictionary as desired. If you publish this alert using + :attr:`pittgoogle.Topic.publish`, this dictionary will be sent as the outgoing + Pub/Sub message's data payload. + + Note: The following is required in order to deserialize the incoming alert bytes. + The bytes can be in either Avro or JSON format, depending on the topic. + If the alert bytes are Avro and contain the schema in the header, the deserialization can + be done without requiring :attr:`Alert.schema`. However, if the alert bytes are + schemaless Avro, the deserialization requires the :attr:`Alert.schema.avsc` attribute to + contain the schema definition. + + Returns: + dict: + The alert data as a dictionary. + + Raises: + OpenAlertError: + If unable to deserialize the alert bytes. """ if self._dict is not None: return self._dict + # [TODO] Add a `required` attribute to types_.Schema (whether the schema is required in order to deserialize the alerts). # deserialize self.msg.data (avro or json bytestring) into a dict. # if self.msg.data is either (1) json; or (2) avro that contains the schema in the header, # self.schema is not required for deserialization, so we want to be lenient. @@ -303,24 +312,24 @@ def dataframe(self) -> "pd.DataFrame": return self._dataframe @property - def alertid(self) -> Union[str, int]: - """Convenience property to get the alert ID. + def alertid(self) -> str | int: + """Return the alert ID. Convenience wrapper around :attr:`Alert.get`. If the survey does not define an alert ID, this returns the `sourceid`. """ return self.get("alertid", self.sourceid) @property - def objectid(self) -> Union[str, int]: - """Convenience property to get the object ID. + def objectid(self) -> str | int: + """Return the object ID. Convenience wrapper around :attr:`Alert.get`. The "object" represents a collection of sources, as determined by the survey. """ return self.get("objectid") @property - def sourceid(self) -> Union[str, int]: - """Convenience property to get the source ID. + def sourceid(self) -> str | int: + """Return the source ID. Convenience wrapper around :attr:`Alert.get`. The "source" is the detection that triggered the alert. """ @@ -328,12 +337,11 @@ def sourceid(self) -> Union[str, int]: @property def schema(self) -> types_.Schema: - """Loads the schema from the registry :class:`pittgoogle.registry.Schemas`. + """Return the schema from the :class:`pittgoogle.Schemas` registry. - Raises - ------ - :class:`pittgoogle.exceptions.SchemaNotFoundError` - if the `schema_name` is not supplied or a schema with this name is not found + Raises: + pittgoogle.exceptions.SchemaNotFoundError: + If the `schema_name` is not supplied or a schema with this name is not found. """ if self._schema is not None: return self._schema @@ -348,7 +356,7 @@ def schema(self) -> types_.Schema: # ---- methods ---- # def add_id_attributes(self) -> None: - """Add the IDs to the attributes. ("alertid", "objectid", "sourceid")""" + """Add the IDs ("alertid", "objectid", "sourceid") to :attr:`Alert.attributes`.""" ids = ["alertid", "objectid", "sourceid"] values = [self.get(id) for id in ids] @@ -364,24 +372,19 @@ def add_id_attributes(self) -> None: self.attributes[idname] = idvalue def get(self, field: str, default: Any = None) -> Any: - """Return the value of `field` in this alert. - - The keys in the alert dictionary :attr:`pittgoogle.alert.Alert.dict` are survey-specific field names. - This method allows you to `get` values from the dict using generic names that will work across - surveys. `self.schema.map` is the mapping of generic -> survey-specific names. - To access a field using a survey-specific name, get it directly from the alert `dict`. - - Parameters - ---------- - field : str - Name of a field in the alert's schema. This must be one of the keys in the dict `self.schema.map`. - default : str or None - Default value to be returned if the field is not found. - - Returns - ------- - value : any - Value in the :attr:`pittgoogle.alert.Alert.dict` corresponding to this field. + """Return the value of a field from the alert data. + + Parameters: + field (str): + Name of a field. This must be one of the generic field names used by Pitt-Google + (keys in :attr:`Alert.schema.map`). To use a survey-specific field name instead, use + :attr:`Alert.dict.get`. + default (str, optional): + The default value to be returned if the field is not found. + + Returns: + any: + The value in the :attr:`Alert.dict` corresponding to the field. """ survey_field = self.schema.map.get(field) # str, list[str], or None @@ -416,27 +419,25 @@ def get(self, field: str, default: Any = None) -> Any: ) def get_key( - self, field: str, name_only: bool = False, default: Optional[str] = None - ) -> Optional[Union[str, list[str]]]: + self, field: str, name_only: bool = False, default: str | None = None + ) -> str | list[str] | None: """Return the survey-specific field name. - Parameters - ---------- - field : str - Generic field name whose survey-specific name is to be returned. This must be one of the - keys in the dict `self.schema.map`. - name_only : bool - In case the survey-specific field name is nested below the top level, whether to return - just the single final name as a str (True) or the full path as a list[str] (False). - default : str or None - Default value to be returned if the field is not found. - - Returns - ------- - survey_field : str or list[str] - Survey-specific name for the `field`, or `default` if the field is not found. - list[str] if this is a nested field and `name_only` is False, else str with the - final field name only. + Args: + field (str): + Generic field name whose survey-specific name is to be returned. This must be one of the + keys in the dict `self.schema.map`. + name_only (bool): + In case the survey-specific field name is nested below the top level, whether to return + just the single final name as a str (True) or the full path as a list[str] (False). + default (str or None): + Default value to be returned if the field is not found. + + Returns: + str or list[str]): + Survey-specific name for the `field`, or `default` if the field is not found. + list[str] if this is a nested field and `name_only` is False, else str with the + final field name only. """ survey_field = self.schema.map.get(field) # str, list[str], or None diff --git a/pittgoogle/auth.py b/pittgoogle/auth.py index e436b43..5556d26 100644 --- a/pittgoogle/auth.py +++ b/pittgoogle/auth.py @@ -1,41 +1,12 @@ # -*- coding: UTF-8 -*- -"""A class to handle authentication with Google Cloud. - -.. contents:: - :local: - :depth: 2 +"""Classes to manage authentication with Google Cloud. .. note:: To authenticate, you must have completed one of the setup options described in - :doc:`/main/one-time-setup/authentication`. The recommended workflow is to use a + :doc:`/main/one-time-setup/authentication`. The recommendation is to use a :ref:`service account ` and :ref:`set environment variables `. In that case, you will not need to call this module directly. - -Usage Example --------------- - -The basic call is: - -.. code-block:: python - - import pittgoogle - - myauth = pittgoogle.auth.Auth() - -This will load authentication settings from your :ref:`environment variables `. -You can override this behavior with keyword arguments. This does not automatically load the -credentials. To do that, request them explicitly: - -.. code-block:: python - - myauth.credentials - -It will first look for a service account key file, then fallback to OAuth2. - -API ----- - """ import logging import os @@ -56,28 +27,51 @@ @define class Auth: - """Credentials for authentication to a Google Cloud project. + """Credentials for authenticating with a Google Cloud project. + + This class provides methods to obtain and load credentials from either a service account + key file or an OAuth2 session. + To authenticate, you must have completed one of the setup options described in the + :doc:`/main/one-time-setup/authentication`.:doc:`/main/one-time-setup/authentication` + + Attributes + ---------- + GOOGLE_CLOUD_PROJECT : str + The project ID of the Google Cloud project to connect to. This can be set as an + environment variable. + + GOOGLE_APPLICATION_CREDENTIALS : str + The path to a keyfile containing service account credentials. Either this or the + `OAUTH_CLIENT_*` settings are required for successful authentication. + + OAUTH_CLIENT_ID : str + The client ID for an OAuth2 connection. Either this and `OAUTH_CLIENT_SECRET`, or + the `GOOGLE_APPLICATION_CREDENTIALS` setting, are required for successful + authentication. + + OAUTH_CLIENT_SECRET : str + The client secret for an OAuth2 connection. Either this and `OAUTH_CLIENT_ID`, or + the `GOOGLE_APPLICATION_CREDENTIALS` setting, are required for successful + authentication. + + Usage + ----- + + The basic call is: + + .. code-block:: python - Missing parameters will be obtained from an environment variable of the same name, - if it exists. + myauth = pittgoogle.Auth() - :param GOOGLE_CLOUD_PROJECT: - Project ID of the Google Cloud project to connect to. + This will load authentication settings from your :ref:`environment variables `. + You can override this behavior with keyword arguments. This does not automatically load the + credentials. To do that, request them explicitly: - :param GOOGLE_APPLICATION_CREDENTIALS: - Path to a keyfile containing service account credentials. - Either this or both `OAUTH_CLIENT_*` settings are required for successful - authentication using `Auth`. + .. code-block:: python - :param OAUTH_CLIENT_ID: - Client ID for an OAuth2 connection. - Either this and `OAUTH_CLIENT_SECRET`, or the `GOOGLE_APPLICATION_CREDENTIALS` - setting, are required for successful authentication using `Auth`. + myauth.credentials - :param OAUTH_CLIENT_SECRET: - Client secret for an OAuth2 connection. - Either this and `OAUTH_CLIENT_ID`, or the `GOOGLE_APPLICATION_CREDENTIALS` setting, - are required for successful authentication using `Auth`. + It will first look for a service account key file, then fallback to OAuth2. """ GOOGLE_CLOUD_PROJECT = field(factory=lambda: os.getenv("GOOGLE_CLOUD_PROJECT", None)) diff --git a/pittgoogle/bigquery.py b/pittgoogle/bigquery.py index f55ae3f..6d46d6d 100644 --- a/pittgoogle/bigquery.py +++ b/pittgoogle/bigquery.py @@ -1,29 +1,11 @@ # -*- coding: UTF-8 -*- """Classes to facilitate connections to BigQuery datasets and tables. -.. contents:: - :local: - :depth: 2 - .. note:: This module relies on :mod:`pittgoogle.auth` to authenticate API calls. The examples given below assume the use of a :ref:`service account ` and - :ref:`environment variables `. In this case, :mod:`pittgoogle.auth` does not - need to be called explicitly. - -Usage Examples ---------------- - -.. code-block:: python - - import pittgoogle - - [TODO] - -API ----- - + :ref:`environment variables `. """ import logging from typing import Optional, Union diff --git a/pittgoogle/pubsub.py b/pittgoogle/pubsub.py index f949d9f..065b499 100644 --- a/pittgoogle/pubsub.py +++ b/pittgoogle/pubsub.py @@ -1,83 +1,11 @@ # -*- coding: UTF-8 -*- """Classes to facilitate connections to Pub/Sub streams. -.. contents:: - :local: - :depth: 2 - .. note:: This module relies on :mod:`pittgoogle.auth` to authenticate API calls. The examples given below assume the use of a :ref:`service account ` and - :ref:`environment variables `. In this case, :mod:`pittgoogle.auth` does not - need to be called explicitly. - -Usage Examples ---------------- - -.. code-block:: python - - import pittgoogle - -Create a subscription to the "ztf-loop" topic: - -.. code-block:: python - - # topic the subscription will be connected to - # only required if the subscription does not yet exist in Google Cloud - topic = pittgoogle.Topic(name="ztf-loop", projectid=pittgoogle.ProjectIds.pittgoogle) - - # choose your own name for the subscription - subscription = pittgoogle.Subscription(name="my-ztf-loop-subscription", topic=topic, schema_name="ztf") - - # make sure the subscription exists and we can connect to it. create it if necessary - subscription.touch() - -Pull a small batch of alerts. Helpful for testing. Not recommended for long-runnining listeners. - -.. code-block:: python - - alerts = pittgoogle.pubsub.pull_batch(subscription, max_messages=4) - -Open a streaming pull. Recommended for long-runnining listeners. This will pull and process -messages in the background, indefinitely. User must supply a callback that processes a single message. -It should accept a :class:`pittgoogle.pubsub.Alert` and return a :class:`pittgoogle.pubsub.Response`. -Optionally, can provide a callback that processes a batch of messages. Note that messages are -acknowledged (and thus permanently deleted) _before_ the batch callback runs, so it is recommended -to do as much processing as possible in the message callback and use a batch callback only when -necessary. - -.. code-block:: python - - def my_msg_callback(alert): - # process the message here. we'll just print the ID. - print(f"processing message: {alert.metadata['message_id']}") - - # return a Response. include a result if using a batch callback. - return pittgoogle.pubsub.Response(ack=True, result=alert.dict) - - def my_batch_callback(results): - # process the batch of results (list of results returned by my_msg_callback) - # we'll just print the number of results in the batch - print(f"batch processing {len(results)} results) - - consumer = pittgoogle.pubsub.Consumer( - subscription=subscription, msg_callback=my_msg_callback, batch_callback=my_batch_callback - ) - - # open the stream in the background and process messages through the callbacks - # this blocks indefinitely. use `Ctrl-C` to close the stream and unblock - consumer.stream() - -Delete the subscription from Google Cloud. - -.. code-block:: python - - subscription.delete() - -API ----- - + :ref:`environment variables `. """ import datetime import importlib.resources @@ -334,25 +262,46 @@ def publish(self, alert: "Alert") -> int: @define class Subscription: - """Basic attributes of a Pub/Sub subscription and methods to manage it. + """Creates a Pub/Sub subscription and provides methods to manage it. - Parameters - ----------- - name : `str` - Name of the Pub/Sub subscription. - auth : :class:`pittgoogle.auth.Auth`, optional - Credentials for the Google Cloud project that owns this subscription. If not provided, - it will be created from environment variables. - topic : :class:`pittgoogle.pubsub.Topic`, optional - Topic this subscription should be attached to. Required only when the subscription needs - to be created. - client : `pubsub_v1.SubscriberClient`, optional - Pub/Sub client that will be used to access the subscription. This kwarg is useful if you - want to reuse a client. If None, a new client will be created. - schema_name : `str` - One of "ztf", "ztf.lite", "elasticc.v0_9_1.alert", "elasticc.v0_9_1.brokerClassification". - Schema name of the alerts in the subscription. Passed to :class:`pittgoogle.pubsub.Alert` - for unpacking. If not provided, some properties of the `Alert` may not be available. + Args: + name (str): + Name of the Pub/Sub subscription. + auth (pittgoogle.auth.Auth, optional): + Credentials for the Google Cloud project that owns this subscription. If not provided, it will be created + from environment variables. + topic (pittgoogle.pubsub.Topic, optional): + Topic this subscription should be attached to. Required only when the subscription needs to be created. + client (google.cloud.pubsub_v1.SubscriberClient, optional): + Pub/Sub client that will be used to access the subscription. This kwarg is useful if you want to + reuse a client. If None, a new client will be created. + schema_name (str): + Schema name of the alerts in the subscription. Passed to :class:`pittgoogle.Alert` for unpacking. + If not provided, some properties of the Alert may not be available. For a list of schema names, see + :meth:`pittgoogle.Schemas.names`. + + Usage: + + Create a subscription to the "ztf-loop" topic: + + .. code-block:: python + + # topic the subscription will be connected to + # only required if the subscription does not yet exist in Google Cloud + topic = pittgoogle.Topic(name="ztf-loop", projectid=pittgoogle.ProjectIds.pittgoogle) + + # choose your own name for the subscription + subscription = pittgoogle.Subscription(name="my-ztf-loop-subscription", topic=topic, schema_name="ztf") + + # make sure the subscription exists and we can connect to it. create it if necessary + subscription.touch() + + Pull a small batch of alerts. Helpful for testing. (For long-runnining listeners, see + :class:`pittgoogle.Consumer`.) + + .. code-block:: python + + alerts = subscription.pull_batch(subscription, max_messages=4) """ name: str = field() @@ -481,27 +430,57 @@ def purge(self): class Consumer: """Consumer class to pull a Pub/Sub subscription and process messages. - Parameters - ----------- - subscription : `str` or :class:`pittgoogle.pubsub.Subscription` - Pub/Sub subscription to be pulled (it must already exist in Google Cloud). - msg_callback : `callable` - Function that will process a single message. It should accept a - :class:`pittgoogle.pubsub.Alert` and return a :class:`pittgoogle.pubsub.Response`. - batch_callback : `callable`, optional - Function that will process a batch of results. It should accept a list of the results - returned by the `msg_callback`. - batch_maxn : `int`, optional - Maximum number of messages in a batch. This has no effect if `batch_callback` is None. - batch_max_wait_between_messages : `int`, optional - Max number of seconds to wait between messages before before processing a batch. - This has no effect if `batch_callback` is None. - max_backlog : `int`, optional - Maximum number of pulled but unprocessed messages before pausing the pull. - max_workers : `int`, optional - Maximum number of workers for the `executor`. This has no effect if an `executor` is provided. - executor : `concurrent.futures.ThreadPoolExecutor`, optional - Executor to be used by the Google API to pull and process messages in the background. + Args: + subscription (str or Subscription): + Pub/Sub subscription to be pulled (it must already exist in Google Cloud). + msg_callback (callable): + Function that will process a single message. It should accept a Alert and return a Response. + batch_callback (callable, optional): + Function that will process a batch of results. It should accept a list of the results + returned by the msg_callback. + batch_maxn (int, optional): + Maximum number of messages in a batch. This has no effect if batch_callback is None. + batch_max_wait_between_messages (int, optional): + Max number of seconds to wait between messages before processing a batch. This has + no effect if batch_callback is None. + max_backlog (int, optional): + Maximum number of pulled but unprocessed messages before pausing the pull. + max_workers (int, optional): + Maximum number of workers for the executor. This has no effect if an executor is provided. + executor (concurrent.futures.ThreadPoolExecutor, optional): + Executor to be used by the Google API to pull and process messages in the background. + + Usage: + Open a streaming pull. Recommended for long-running listeners. This will pull and process + messages in the background, indefinitely. User must supply a callback that processes a single message. + It should accept a :class:`pittgoogle.pubsub.Alert` and return a :class:`pittgoogle.pubsub.Response`. + Optionally, can provide a callback that processes a batch of messages. Note that messages are + acknowledged (and thus permanently deleted) _before_ the batch callback runs, so it is recommended + to do as much processing as possible in the message callback and use a batch callback only when + necessary. + + .. code-block:: python + + def my_msg_callback(alert): + # process the message here. we'll just print the ID. + print(f"processing message: {alert.metadata['message_id']}") + + # return a Response. include a result if using a batch callback. + return pittgoogle.pubsub.Response(ack=True, result=alert.dict) + + def my_batch_callback(results): + # process the batch of results (list of results returned by my_msg_callback) + # we'll just print the number of results in the batch + print(f"batch processing {len(results)} results) + + consumer = pittgoogle.pubsub.Consumer( + subscription=subscription, msg_callback=my_msg_callback, batch_callback=my_batch_callback + ) + + # open the stream in the background and process messages through the callbacks + # this blocks indefinitely. use `Ctrl-C` to close the stream and unblock + consumer.stream() + """ _subscription: Union[str, Subscription] = field(validator=instance_of((str, Subscription))) diff --git a/pittgoogle/types_.py b/pittgoogle/types_.py index f972cdb..a7da725 100644 --- a/pittgoogle/types_.py +++ b/pittgoogle/types_.py @@ -1,5 +1,5 @@ # -*- coding: UTF-8 -*- -"""Functions to support working with alerts and related data.""" +"""Classes defining new types.""" import importlib.resources import logging from typing import TYPE_CHECKING, Optional @@ -21,7 +21,7 @@ class Schema: """Class for an individual schema. This class is not intended to be used directly. - Use `pittgoogle.registry.Schemas` instead. + Use :class:`pittgoogle.Schemas` instead. """ name: str = field() diff --git a/pittgoogle/utils.py b/pittgoogle/utils.py index cd18980..b4e9467 100644 --- a/pittgoogle/utils.py +++ b/pittgoogle/utils.py @@ -1,5 +1,5 @@ # -*- coding: UTF-8 -*- -"""Functions to support working with alerts and related data.""" +"""Classes and functions to support working with alerts and related data.""" import json import logging from base64 import b64decode, b64encode diff --git a/poetry.lock b/poetry.lock index e0ce3d9..4b3cafa 100644 --- a/poetry.lock +++ b/poetry.lock @@ -309,13 +309,13 @@ toml = ["tomli"] [[package]] name = "docutils" -version = "0.16" +version = "0.20.1" description = "Docutils -- Python Documentation Utilities" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.7" files = [ - {file = "docutils-0.16-py2.py3-none-any.whl", hash = "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af"}, - {file = "docutils-0.16.tar.gz", hash = "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"}, + {file = "docutils-0.20.1-py3-none-any.whl", hash = "sha256:96f387a2c5562db4476f09f13bbab2192e764cac08ebbf3a34a95d9b1e4a59d6"}, + {file = "docutils-0.20.1.tar.gz", hash = "sha256:f08a4e276c3a1583a86dce3e34aba3fe04d02bba2dd51ed16106244e8a923e3b"}, ] [[package]] @@ -777,13 +777,13 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "markdown-it-py" -version = "2.2.0" +version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "markdown-it-py-2.2.0.tar.gz", hash = "sha256:7c9a5e412688bc771c67432cbfebcdd686c93ce6484913dccf06cb5a0bea35a1"}, - {file = "markdown_it_py-2.2.0-py3-none-any.whl", hash = "sha256:5a35f8d1870171d9acc47b99612dc146129b631baf04970128b568f190d0cc30"}, + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, ] [package.dependencies] @@ -796,7 +796,7 @@ compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0 linkify = ["linkify-it-py (>=1,<3)"] plugins = ["mdit-py-plugins"] profiling = ["gprof2dot"] -rtd = ["attrs", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] @@ -870,21 +870,21 @@ files = [ [[package]] name = "mdit-py-plugins" -version = "0.3.5" +version = "0.4.1" description = "Collection of plugins for markdown-it-py" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "mdit-py-plugins-0.3.5.tar.gz", hash = "sha256:eee0adc7195e5827e17e02d2a258a2ba159944a0748f59c5099a4a27f78fcf6a"}, - {file = "mdit_py_plugins-0.3.5-py3-none-any.whl", hash = "sha256:ca9a0714ea59a24b2b044a1831f48d817dd0c817e84339f20e7889f392d77c4e"}, + {file = "mdit_py_plugins-0.4.1-py3-none-any.whl", hash = "sha256:1020dfe4e6bfc2c79fb49ae4e3f5b297f5ccd20f010187acc52af2921e27dc6a"}, + {file = "mdit_py_plugins-0.4.1.tar.gz", hash = "sha256:834b8ac23d1cd60cec703646ffd22ae97b7955a6d596eb1d304be1e251ae499c"}, ] [package.dependencies] -markdown-it-py = ">=1.0.0,<3.0.0" +markdown-it-py = ">=1.0.0,<4.0.0" [package.extras] code-style = ["pre-commit"] -rtd = ["attrs", "myst-parser (>=0.16.1,<0.17.0)", "sphinx-book-theme (>=0.1.0,<0.2.0)"] +rtd = ["myst-parser", "sphinx-book-theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] [[package]] @@ -900,29 +900,29 @@ files = [ [[package]] name = "myst-parser" -version = "1.0.0" +version = "3.0.1" description = "An extended [CommonMark](https://spec.commonmark.org/) compliant parser," optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "myst-parser-1.0.0.tar.gz", hash = "sha256:502845659313099542bd38a2ae62f01360e7dd4b1310f025dd014dfc0439cdae"}, - {file = "myst_parser-1.0.0-py3-none-any.whl", hash = "sha256:69fb40a586c6fa68995e6521ac0a525793935db7e724ca9bac1d33be51be9a4c"}, + {file = "myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1"}, + {file = "myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87"}, ] [package.dependencies] -docutils = ">=0.15,<0.20" +docutils = ">=0.18,<0.22" jinja2 = "*" -markdown-it-py = ">=1.0.0,<3.0.0" -mdit-py-plugins = ">=0.3.4,<0.4.0" +markdown-it-py = ">=3.0,<4.0" +mdit-py-plugins = ">=0.4,<1.0" pyyaml = "*" -sphinx = ">=5,<7" +sphinx = ">=6,<8" [package.extras] code-style = ["pre-commit (>=3.0,<4.0)"] -linkify = ["linkify-it-py (>=1.0,<2.0)"] -rtd = ["ipython", "pydata-sphinx-theme (==v0.13.0rc4)", "sphinx-autodoc2 (>=0.4.2,<0.5.0)", "sphinx-book-theme (==1.0.0rc2)", "sphinx-copybutton", "sphinx-design2", "sphinx-pyscript", "sphinx-tippy (>=0.3.1)", "sphinx-togglebutton", "sphinxext-opengraph (>=0.7.5,<0.8.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] -testing = ["beautifulsoup4", "coverage[toml]", "pytest (>=7,<8)", "pytest-cov", "pytest-param-files (>=0.3.4,<0.4.0)", "pytest-regressions", "sphinx-pytest"] -testing-docutils = ["pygments", "pytest (>=7,<8)", "pytest-param-files (>=0.3.4,<0.4.0)"] +linkify = ["linkify-it-py (>=2.0,<3.0)"] +rtd = ["ipython", "sphinx (>=7)", "sphinx-autodoc2 (>=0.5.0,<0.6.0)", "sphinx-book-theme (>=1.1,<2.0)", "sphinx-copybutton", "sphinx-design", "sphinx-pyscript", "sphinx-tippy (>=0.4.3)", "sphinx-togglebutton", "sphinxext-opengraph (>=0.9.0,<0.10.0)", "sphinxext-rediraffe (>=0.2.7,<0.3.0)"] +testing = ["beautifulsoup4", "coverage[toml]", "defusedxml", "pytest (>=8,<9)", "pytest-cov", "pytest-param-files (>=0.6.0,<0.7.0)", "pytest-regressions", "sphinx-pytest"] +testing-docutils = ["pygments", "pytest (>=8,<9)", "pytest-param-files (>=0.6.0,<0.7.0)"] [[package]] name = "numpy" @@ -1333,67 +1333,49 @@ files = [ [[package]] name = "sphinx" -version = "5.3.0" +version = "7.3.7" description = "Python documentation generator" optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" files = [ - {file = "Sphinx-5.3.0.tar.gz", hash = "sha256:51026de0a9ff9fc13c05d74913ad66047e104f56a129ff73e174eb5c3ee794b5"}, - {file = "sphinx-5.3.0-py3-none-any.whl", hash = "sha256:060ca5c9f7ba57a08a1219e547b269fadf125ae25b06b9fa7f66768efb652d6d"}, + {file = "sphinx-7.3.7-py3-none-any.whl", hash = "sha256:413f75440be4cacf328f580b4274ada4565fb2187d696a84970c23f77b64d8c3"}, + {file = "sphinx-7.3.7.tar.gz", hash = "sha256:a4a7db75ed37531c05002d56ed6948d4c42f473a36f46e1382b0bd76ca9627bc"}, ] [package.dependencies] -alabaster = ">=0.7,<0.8" +alabaster = ">=0.7.14,<0.8.0" babel = ">=2.9" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.14,<0.20" +docutils = ">=0.18.1,<0.22" imagesize = ">=1.3" importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} Jinja2 = ">=3.0" packaging = ">=21.0" -Pygments = ">=2.12" -requests = ">=2.5.0" +Pygments = ">=2.14" +requests = ">=2.25.0" snowballstemmer = ">=2.0" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" -sphinxcontrib-serializinghtml = ">=1.1.5" +sphinxcontrib-serializinghtml = ">=1.1.9" +tomli = {version = ">=2", markers = "python_version < \"3.11\""} [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "flake8-simplify", "isort", "mypy (>=0.981)", "sphinx-lint", "types-requests", "types-typed-ast"] -test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] - -[[package]] -name = "sphinx-autodoc-typehints" -version = "1.22" -description = "Type hints (PEP 484) support for the Sphinx autodoc extension" -optional = false -python-versions = ">=3.7" -files = [ - {file = "sphinx_autodoc_typehints-1.22-py3-none-any.whl", hash = "sha256:ef4a8b9d52de66065aa7d3adfabf5a436feb8a2eff07c2ddc31625d8807f2b69"}, - {file = "sphinx_autodoc_typehints-1.22.tar.gz", hash = "sha256:71fca2d5eee9b034204e4c686ab20b4d8f5eb9409396216bcae6c87c38e18ea6"}, -] - -[package.dependencies] -sphinx = ">=5.3" - -[package.extras] -docs = ["furo (>=2022.12.7)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.21)"] -testing = ["covdefaults (>=2.2.2)", "coverage (>=7.0.5)", "diff-cover (>=7.3)", "nptyping (>=2.4.1)", "pytest (>=7.2.1)", "pytest-cov (>=4)", "sphobjinv (>=2.3.1)", "typing-extensions (>=4.4)"] -type-comment = ["typed-ast (>=1.5.4)"] +lint = ["flake8 (>=3.5.0)", "importlib_metadata", "mypy (==1.9.0)", "pytest (>=6.0)", "ruff (==0.3.7)", "sphinx-lint", "tomli", "types-docutils", "types-requests"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=6.0)", "setuptools (>=67.0)"] [[package]] name = "sphinx-copybutton" -version = "0.5.1" +version = "0.5.2" description = "Add a copy button to each of your code cells." optional = false python-versions = ">=3.7" files = [ - {file = "sphinx-copybutton-0.5.1.tar.gz", hash = "sha256:366251e28a6f6041514bfb5439425210418d6c750e98d3a695b73e56866a677a"}, - {file = "sphinx_copybutton-0.5.1-py3-none-any.whl", hash = "sha256:0842851b5955087a7ec7fc870b622cb168618ad408dee42692e9a5c97d071da8"}, + {file = "sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd"}, + {file = "sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e"}, ] [package.dependencies] @@ -1405,19 +1387,19 @@ rtd = ["ipython", "myst-nb", "sphinx", "sphinx-book-theme", "sphinx-examples"] [[package]] name = "sphinx-rtd-theme" -version = "1.2.0" +version = "2.0.0" description = "Read the Docs theme for Sphinx" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" +python-versions = ">=3.6" files = [ - {file = "sphinx_rtd_theme-1.2.0-py2.py3-none-any.whl", hash = "sha256:f823f7e71890abe0ac6aaa6013361ea2696fc8d3e1fa798f463e82bdb77eeff2"}, - {file = "sphinx_rtd_theme-1.2.0.tar.gz", hash = "sha256:a0d8bd1a2ed52e0b338cbe19c4b2eef3c5e7a048769753dac6a9f059c7b641b8"}, + {file = "sphinx_rtd_theme-2.0.0-py2.py3-none-any.whl", hash = "sha256:ec93d0856dc280cf3aee9a4c9807c60e027c7f7b461b77aeffed682e68f0e586"}, + {file = "sphinx_rtd_theme-2.0.0.tar.gz", hash = "sha256:bd5d7b80622406762073a04ef8fadc5f9151261563d47027de09910ce03afe6b"}, ] [package.dependencies] -docutils = "<0.19" -sphinx = ">=1.6,<7" -sphinxcontrib-jquery = {version = ">=2.0.0,<3.0.0 || >3.0.0", markers = "python_version > \"3\""} +docutils = "<0.21" +sphinx = ">=5,<8" +sphinxcontrib-jquery = ">=4,<5" [package.extras] dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client", "wheel"] @@ -1544,6 +1526,17 @@ files = [ [package.extras] widechars = ["wcwidth"] +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + [[package]] name = "tzdata" version = "2024.1" @@ -1590,4 +1583,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "c93822a93d2dd043082ac96eb393aa9d4da3371bc1783d8a326375f923d720b8" +content-hash = "8cca48c9ce56987e503a2f237998de4c9e2456e48bbbebcf60b40ec7d7ea0f3b" diff --git a/pyproject.toml b/pyproject.toml index 9c1da85..491d350 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -42,12 +42,12 @@ tabulate = ">=0.9" optional = true [tool.poetry.group.docs.dependencies] -docutils = "<0.17" # render lists properly -myst-parser = "*" -sphinx = "5.3.0" -sphinx-copybutton = "0.5.1" -sphinx_autodoc_typehints = "1.22" -sphinx_rtd_theme = "1.2.0" +docutils = ">=0.20" +myst-parser = ">=3.0" +sphinx = ">=7.0" +sphinx-copybutton = ">=0.5.1" +sphinx_rtd_theme = ">=2.0" +# sphinx_autodoc_typehints = ">=2.0" # see docs/conf.py [tool.poetry.group.tests] optional = true