Skip to content

Commit

Permalink
Improve documentation (#45)
Browse files Browse the repository at this point in the history
* improve how docs are being rendered

* improve docstring clarity and rendering

* Alert add_id_attributes -> _add_id_attributes

* update bigquery docstrings and signature definitions

* improve Auth docstrings, signatures, type hints

* import full modules for clarity

* use Google style guide for utils docstrings
  • Loading branch information
troyraen authored Jun 30, 2024
1 parent 8339adf commit 626ec45
Show file tree
Hide file tree
Showing 16 changed files with 332 additions and 316 deletions.
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)

## \[Unreleased\]

(none)
<!-- (none) -->
### Changed

- Make `Alert` method private, `add_id_attributes` -> `_add_id_attributes`.
- Update docstrings for clarity and accuracy.
- Improve type hints.
- Fix up Sphinx and rst to improve how docs are being rendered.

## \[v0.3.4\] - 2024-06-29

Expand Down
2 changes: 0 additions & 2 deletions docs/source/api-reference/alert.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,3 @@ pittgoogle.alert

.. automodule:: pittgoogle.alert
:members:
:private-members:
:member-order: bysource
2 changes: 0 additions & 2 deletions docs/source/api-reference/auth.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,3 @@ pittgoogle.auth

.. automodule:: pittgoogle.auth
:members:
:private-members:
:member-order: bysource
2 changes: 0 additions & 2 deletions docs/source/api-reference/exceptions.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,3 @@ pittgoogle.exceptions

.. automodule:: pittgoogle.exceptions
:members:
:private-members:
:member-order: bysource
18 changes: 10 additions & 8 deletions docs/source/api-reference/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@ pittgoogle
.. autosummary::

pittgoogle.Alert
pittgoogle.Auth
pittgoogle.Consumer
pittgoogle.ProjectIds
pittgoogle.Schemas
pittgoogle.Subscription
pittgoogle.Table
pittgoogle.Topic
.. autosummary::

pittgoogle.alert.Alert
pittgoogle.auth.Auth
pittgoogle.bigquery.Table
pittgoogle.pubsub.Consumer
pittgoogle.pubsub.Subscription
pittgoogle.pubsub.Topic
pittgoogle.registry.ProjectIds
pittgoogle.registry.Schemas
2 changes: 0 additions & 2 deletions docs/source/api-reference/registry.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,3 @@ pittgoogle.registry

.. automodule:: pittgoogle.registry
:members:
:private-members:
:member-order: bysource
2 changes: 0 additions & 2 deletions docs/source/api-reference/types_.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,3 @@ pittgoogle.types_

.. automodule:: pittgoogle.types_
:members:
:private-members:
:member-order: bysource
1 change: 0 additions & 1 deletion docs/source/api-reference/utils.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,3 @@ pittgoogle.utils

.. automodule:: pittgoogle.utils
:members:
:member-order: bysource
1 change: 0 additions & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ If you run into trouble, please
:maxdepth: 1

listings
Install<one-time-setup/install>
one-time-setup/index
faq/index
for-developers/index
Expand Down
2 changes: 1 addition & 1 deletion docs/source/one-time-setup/install.rst
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
.. _install:

Install pittgoogle-client
-------------------------
=========================

.. automodule:: pittgoogle

Expand Down
100 changes: 46 additions & 54 deletions pittgoogle/alert.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
from typing import TYPE_CHECKING, Any, Mapping, Union

import fastavro
import google.cloud.pubsub_v1
from attrs import define, field

from . import registry, types_, utils
from .exceptions import BadRequest, OpenAlertError, SchemaNotFoundError

if TYPE_CHECKING:
import google.cloud.pubsub_v1
import pandas as pd # always lazy-load pandas. it hogs memory on cloud functions and run

LOGGER = logging.getLogger(__name__)
Expand All @@ -26,39 +26,37 @@
class 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
:meth:`pittgoogle.Alert.from_dict`.
All parameters are keyword only.
To create an `Alert`, use one of the `from_*` methods like :meth:`pittgoogle.Alert.from_dict`.
Instances of this class are also returned by other calls like :meth:`pittgoogle.pubsub.Subscription.pull_batch`.
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
`<https://cloud.google.com/python/docs/reference/pubsub/latest/google.cloud.pubsub_v1.types.PubsubMessage>`__.
The alert data as a dictionary. If not provided, it will be loaded from the
attributes (dict, optional):
Attributes or custom metadata for the alert.
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.
Name of the schema for the alert. This is use to deserialize the alert bytes.
See :meth:`pittgoogle.registry.Schemas.names` for a list of options.
If not provided, some properties of the `Alert` may not be available.
msg (PubsubMessageLike or google.cloud.pubsub_v1.types.PubsubMessage, optional):
The incoming Pub/Sub message object. This class is documented at
`<https://cloud.google.com/python/docs/reference/pubsub/latest/google.cloud.pubsub_v1.types.PubsubMessage>`__.
path (pathlib.Path, optional):
Path to a file containing the alert data.
----
"""

# 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)
_attributes: Mapping[str, str] | None = field(default=None)
schema_name: str | None = field(default=None)
_schema: types_.Schema | None = field(default=None, init=False)
msg: google.cloud.pubsub_v1.types.PubsubMessage | types_.PubsubMessageLike | None = field(
default=None
)
path: Path | None = field(default=None)
# Use "Union" because " | " is throwing an error when combined with forward references.
_dataframe: Union["pd.DataFrame", None] = field(default=None)
_schema: types_.Schema | None = field(default=None, init=False)

# ---- class methods ---- #
@classmethod
Expand All @@ -80,33 +78,34 @@ def from_cloud_run(cls, envelope: Mapping, schema_name: str | None = None) -> "A
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:
.. code-block:: python
.. code-block:: python
import pittgoogle
# flask is used to work with HTTP requests, which trigger Cloud Run modules
# the request contains the Pub/Sub message, which contains the alert packet
import flask
import pittgoogle
# flask is used to work with HTTP requests, which trigger Cloud Run modules
# the request contains the Pub/Sub message, which contains the alert packet
import flask
app = flask.Flask(__name__)
app = flask.Flask(__name__)
# function that receives the request
@app.route("/", methods=["POST"])
def index():
# function that receives the request
@app.route("/", methods=["POST"])
def index():
try:
# unpack the alert
# if the request does not contain a valid message, this raises a `BadRequest`
alert = pittgoogle.Alert.from_cloud_run(envelope=flask.request.get_json(), schema_name="ztf")
try:
# unpack the alert
# if the request does not contain a valid message, this raises a `BadRequest`
alert = pittgoogle.Alert.from_cloud_run(envelope=flask.request.get_json(), schema_name="ztf")
except pittgoogle.exceptions.BadRequest as exc:
# return the error text and an HTTP 400 Bad Request code
return str(exc), 400
except pittgoogle.exceptions.BadRequest as exc:
# return the error text and an HTTP 400 Bad Request code
return str(exc), 400
# continue processing the alert
# when finished, return an empty string and an HTTP success code
return "", 204
# continue processing the alert
# when finished, return an empty string and an HTTP success code
return "", 204
"""
# check whether received message is valid, as suggested by Cloud Run docs
if not envelope:
Expand Down Expand Up @@ -219,22 +218,15 @@ def attributes(self) -> Mapping:

@property
def dict(self) -> Mapping:
"""Return the alert data as a dictionary.
"""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.
alert bytes from :attr:`Alert.msg.data`.
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.
Expand Down Expand Up @@ -355,7 +347,7 @@ def schema(self) -> types_.Schema:
return self._schema

# ---- methods ---- #
def add_id_attributes(self) -> None:
def _add_id_attributes(self) -> None:
"""Add the IDs ("alertid", "objectid", "sourceid") to :attr:`Alert.attributes`."""
ids = ["alertid", "objectid", "sourceid"]
values = [self.get(id) for id in ids]
Expand Down
96 changes: 51 additions & 45 deletions pittgoogle/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,90 +10,96 @@
"""
import logging
import os
from typing import TYPE_CHECKING, Union

import attrs
import google.auth
import google.auth.credentials
import google.oauth2.credentials
import google_auth_oauthlib.helpers
from attrs import define, field
from requests_oauthlib import OAuth2Session

if TYPE_CHECKING:
import google.auth.credentials
import google.oauth2.credentials


LOGGER = logging.getLogger(__name__)


@define
@attrs.define
class Auth:
"""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
:doc:`/one-time-setup/authentication`.
This class provides methods to 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 :doc:`/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.
In typical use cases, the following arguments are set as environment variables instead of
being passed to `Auth` explicitly.
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.
Args:
GOOGLE_CLOUD_PROJECT (str, optional):
The project ID of the Google Cloud project to connect to.
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.
GOOGLE_APPLICATION_CREDENTIALS (str, optional):
The path to a keyfile containing service account credentials. Either this or the
`OAUTH_CLIENT_*` settings 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.
OAUTH_CLIENT_ID (str, optional):
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, optional):
The client secret for an OAuth2 connection. Either this and `OAUTH_CLIENT_ID`, or
the `GOOGLE_APPLICATION_CREDENTIALS` setting, are required for successful
authentication.
Example:
The basic call is:
The basic call is:
.. code-block:: python
.. code-block:: python
myauth = pittgoogle.Auth()
myauth = pittgoogle.Auth()
This will load authentication settings from your :ref:`environment variables <set env vars>`.
You can override this behavior with keyword arguments. This does not automatically load the
credentials. To do that, request them explicitly:
This will load authentication settings from your :ref:`environment variables <set env vars>`.
You can override this behavior with keyword arguments. This does not automatically load the
credentials. To do that, request them explicitly:
.. code-block:: python
.. code-block:: python
myauth.credentials
myauth.credentials
It will first look for a service account key file, then fallback to OAuth2.
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))
GOOGLE_APPLICATION_CREDENTIALS = field(
# Strings _below_ the field will make these also show up as individual properties in rendered docs.
GOOGLE_CLOUD_PROJECT: str | None = attrs.field(
factory=lambda: os.getenv("GOOGLE_CLOUD_PROJECT", None)
)
"""The project ID of the Google Cloud project to connect to."""
GOOGLE_APPLICATION_CREDENTIALS: str | None = attrs.field(
factory=lambda: os.getenv("GOOGLE_APPLICATION_CREDENTIALS", None)
)
OAUTH_CLIENT_ID = field(factory=lambda: os.getenv("OAUTH_CLIENT_ID", None))
OAUTH_CLIENT_SECRET = field(factory=lambda: os.getenv("OAUTH_CLIENT_SECRET", None))
_credentials = field(default=None, init=False)
_oauth2 = field(default=None, init=False)
"""The path to a keyfile containing service account credentials."""
OAUTH_CLIENT_ID: str | None = attrs.field(factory=lambda: os.getenv("OAUTH_CLIENT_ID", None))
"""The client ID for an OAuth2 connection."""
OAUTH_CLIENT_SECRET: str | None = attrs.field(
factory=lambda: os.getenv("OAUTH_CLIENT_SECRET", None)
)
"""The client secret for an OAuth2 connection."""
# The rest don't need string descriptions because they are explicitly defined as properties below.
_credentials = attrs.field(default=None, init=False)
_oauth2 = attrs.field(default=None, init=False)

@property
def credentials(
self,
) -> Union["google.auth.credentials.Credentials", "google.oauth2.credentials.Credentials"]:
) -> google.auth.credentials.Credentials | google.oauth2.credentials.Credentials:
"""Credentials, loaded from a service account key file or an OAuth2 session."""
if self._credentials is None:
self._credentials = self._get_credentials()
return self._credentials

def _get_credentials(
self,
) -> Union["google.auth.credentials.Credentials", "google.oauth2.credentials.Credentials"]:
) -> google.auth.credentials.Credentials | google.oauth2.credentials.Credentials:
"""Load user credentials from a service account key file or an OAuth2 session.
Try the service account first, fall back to OAuth2.
Expand Down
Loading

0 comments on commit 626ec45

Please sign in to comment.