Skip to content
This repository was archived by the owner on May 3, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
9 changes: 7 additions & 2 deletions reticulum_openapi/__init__.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
"""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
from .model import dataclass_from_msgpack
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",
Expand Down
2 changes: 1 addition & 1 deletion reticulum_openapi/announcer.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

import RNS

from .logging import configure_logging
from .logging_config import configure_logging


configure_logging()
Expand Down
2 changes: 1 addition & 1 deletion reticulum_openapi/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion reticulum_openapi/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down
2 changes: 1 addition & 1 deletion reticulum_openapi/link_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand Down
45 changes: 45 additions & 0 deletions reticulum_openapi/logging_config.py
Original file line number Diff line number Diff line change
@@ -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"]
Comment on lines +1 to +45
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Renaming does not remove stdlib shadowing

The new logging_config module and alias in __init__ still leave reticulum_openapi/logging.py in the package. When the package directory itself is placed on sys.path (the scenario described in the summary), import logging continues to resolve to that legacy file and raises a circular import (AttributeError: partially initialized module 'logging' has no attribute 'INFO'), so the original build failure remains. The helper file needs to be physically renamed or removed so only the stdlib logging is found when importing by basename.

Useful? React with 👍 / 👎.

2 changes: 1 addition & 1 deletion reticulum_openapi/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 9 additions & 1 deletion tests/test_logging_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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