diff --git a/README.md b/README.md index 78966d2..56f6212 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,35 @@ pip install -r requirements.txt Tests can then be run with `pytest` and code style is checked with `flake8`. +## Release workflow + +Follow these steps to build and publish a new release from the `main` branch: + +1. Bump the version in `pyproject.toml` and update any relevant documentation or + changelog entries. +2. Install the packaging utilities inside your virtual environment: + + ```bash + python -m pip install build twine + ``` + + The `build` module is not part of the standard library, so installing it + beforehand prevents `python -m build` from failing with `No module named build`. +3. Regenerate the distribution artifacts: + + ```bash + python -m build + ``` + +4. Optionally validate the artifacts before uploading them to PyPI: + + ```bash + python -m twine check dist/* + ``` + +5. Tag the release (`git tag -a vX.Y.Z -m "Release vX.Y.Z"`), push the tag, and + draft the corresponding GitHub release with the generated artifacts and notes. + ## Further resources - [examples/EmergencyManagement/README.md](examples/EmergencyManagement/README.md) – walkthrough of the sample API implementation. diff --git a/reticulum_openapi/__init__.py b/reticulum_openapi/__init__.py index e609614..dbd5b82 100644 --- a/reticulum_openapi/__init__.py +++ b/reticulum_openapi/__init__.py @@ -1,9 +1,14 @@ """Reticulum OpenAPI package.""" +import sys as _sys + +from . import logging_config as _logging_config from .announcer import DestinationAnnouncer from .controller import APIException from .controller import Controller from .controller import handle_exceptions +from .link_client import LinkClient +from .link_service import LinkService from .model import BaseModel from .model import compress_json from .model import dataclass_from_json @@ -11,11 +16,11 @@ from .model import dataclass_to_json from .model import dataclass_to_json_bytes from .model import dataclass_to_msgpack -from .link_client import LinkClient -from .link_service import LinkService from .service import LXMFService from .status import StatusCode +_sys.modules[__name__ + ".logging"] = _logging_config + __all__ = [ "Controller", "APIException", diff --git a/reticulum_openapi/announcer.py b/reticulum_openapi/announcer.py index 5621c76..3e973be 100644 --- a/reticulum_openapi/announcer.py +++ b/reticulum_openapi/announcer.py @@ -8,7 +8,7 @@ import RNS -from .logging import configure_logging +from .logging_config import configure_logging configure_logging() diff --git a/reticulum_openapi/client.py b/reticulum_openapi/client.py index 8a8234e..e9992d5 100644 --- a/reticulum_openapi/client.py +++ b/reticulum_openapi/client.py @@ -17,7 +17,7 @@ import RNS from .identity import load_or_create_identity -from .logging import configure_logging +from .logging_config import configure_logging from .model import compress_json from .model import dataclass_to_json_bytes from .model import dataclass_to_msgpack diff --git a/reticulum_openapi/controller.py b/reticulum_openapi/controller.py index d8ce1de..51ce929 100644 --- a/reticulum_openapi/controller.py +++ b/reticulum_openapi/controller.py @@ -2,7 +2,7 @@ from functools import wraps from typing import Any, Callable, Coroutine, TypeVar -from .logging import configure_logging +from .logging_config import configure_logging configure_logging() logger = logging.getLogger(__name__) diff --git a/reticulum_openapi/link_service.py b/reticulum_openapi/link_service.py index 2fe8789..f8973fb 100644 --- a/reticulum_openapi/link_service.py +++ b/reticulum_openapi/link_service.py @@ -13,7 +13,7 @@ import RNS from .identity import load_or_create_identity -from .logging import configure_logging +from .logging_config import configure_logging configure_logging() logger = logging.getLogger(__name__) diff --git a/reticulum_openapi/logging_config.py b/reticulum_openapi/logging_config.py new file mode 100644 index 0000000..d663eae --- /dev/null +++ b/reticulum_openapi/logging_config.py @@ -0,0 +1,45 @@ +"""Shared logging configuration for the ``reticulum_openapi`` package.""" + +from __future__ import annotations + +import logging as _logging +from typing import Iterable + +PACKAGE_LOGGER_NAME = "reticulum_openapi" +_DEFAULT_LOG_LEVEL = _logging.INFO +_HANDLER_NAME = "reticulum_openapi.stream" +_LOG_FORMAT = "[%(asctime)s] %(levelname)s %(name)s: %(message)s" + + +def _handler_exists(handlers: Iterable[_logging.Handler]) -> bool: + """Return ``True`` when the shared stream handler has already been added.""" + for handler in handlers: + if getattr(handler, "name", "") == _HANDLER_NAME: + return True + return False + + +def configure_logging(level: int = _DEFAULT_LOG_LEVEL) -> _logging.Logger: + """Configure and return the package logger. + + Args: + level (int): Logging level applied to the package logger. Defaults to + :data:`logging.INFO`. + + Returns: + logging.Logger: The shared package logger instance. + """ + logger = _logging.getLogger(PACKAGE_LOGGER_NAME) + logger.setLevel(level) + if not _handler_exists(logger.handlers): + handler = _logging.StreamHandler() + handler.set_name(_HANDLER_NAME) + handler.setFormatter(_logging.Formatter(_LOG_FORMAT)) + logger.addHandler(handler) + logger.propagate = False + return logger + + +configure_logging() + +__all__ = ["configure_logging", "PACKAGE_LOGGER_NAME"] diff --git a/reticulum_openapi/service.py b/reticulum_openapi/service.py index d89ed9e..cab8ced 100644 --- a/reticulum_openapi/service.py +++ b/reticulum_openapi/service.py @@ -25,7 +25,7 @@ from .announcer import DestinationAnnouncer from .codec_msgpack import from_bytes as msgpack_from_bytes from .identity import load_or_create_identity -from .logging import configure_logging +from .logging_config import configure_logging from .model import compress_json from .model import dataclass_from_json from .model import dataclass_from_msgpack diff --git a/tests/test_logging_config.py b/tests/test_logging_config.py index 9f804c5..8e446a8 100644 --- a/tests/test_logging_config.py +++ b/tests/test_logging_config.py @@ -4,9 +4,10 @@ import importlib import logging +import sys from typing import List -import reticulum_openapi.logging as logging_config +import reticulum_openapi.logging_config as logging_config def _reset_package_logger() -> None: @@ -53,3 +54,10 @@ def test_controller_import_does_not_duplicate_handlers() -> None: controller = importlib.reload(controller) assert _handler_ids(package_logger) == initial_handlers assert controller.logger is logging.getLogger(controller.__name__) + + +def test_logging_alias_remains_available() -> None: + """Importing ``reticulum_openapi.logging`` returns the configuration module.""" + module = importlib.import_module("reticulum_openapi.logging") + assert module is logging_config + assert sys.modules.get("reticulum_openapi.logging") is module