Skip to content

Development

Development

This section covers everything you need to set up a local development environment and contribute code to libreclient.

Prerequisites

  • Python 3.12+
  • uv (package manager)

Setup

git clone https://github.com/jjeff07/libreclient.git
cd libreclient
uv sync

Git Hooks

This project uses custom git hooks in the .githooks/ directory.

Setup (once per clone):

git config core.hooksPath .githooks
Hook Purpose
pre-commit Runs ruff check --fix and ruff format on staged .py files, re-stages fixes, then runs complexipy to enforce max cognitive complexity (15).
commit-msg Validates commit messages against Conventional Commits format via commitizen.

Running Tests

# Unit tests
uv run pytest tests/unit

# Functional tests (requires .env with LIBRENMS_URL and LIBRENMS_TOKEN)
uv run pytest tests/functional

Linting & Formatting

This project uses Ruff for both linting and formatting:

# Check for lint issues
uv run ruff check

# Auto-fix lint issues
uv run ruff check --fix

# Format code
uv run ruff format

# Check formatting without changing files
uv run ruff format --check

Complexity Checks

complexipy is used to enforce a maximum cognitive complexity of 15 per function:

uv run complexipy .

Results are output to complexipy-results.json. Any function exceeding the threshold will cause the check to fail.

Architecture

The project uses a single-implementation pattern: each route is written once as an async class. The synchronicity library then wraps each async class to produce a synchronous counterpart at runtime.

src/libreclient/routes/
├── alerts.py           ← async implementation (the only code you write)
└── alerts_sync.py      ← sync wrapper (imports Alerts, wraps with synchronizer)

This means:

  • You only maintain one implementation per route.
  • Both LibreClientAsync and LibreClientSync share the same logic.
  • No code duplication between sync and async interfaces.

Type Stubs

Because synchronicity generates wrapper classes dynamically, IDEs can't infer their method signatures. To restore full autocomplete and type checking, .pyi stub files are auto-generated.

Regenerate stubs locally:

uv run python scripts/generate_stubs.py

Stubs are generated automatically during the GitHub Actions release workflow, so you don't need to commit them — they're in .gitignore.

Adding a New Route

  1. Create src/libreclient/routes/myroute.py with an async class.
  2. Create src/libreclient/routes/myroute_sync.py with MyRouteSync = synchronizer.wrap(...).
  3. Create src/libreclient/models/myroute.py with Pydantic response models.
  4. Add exports to src/libreclient/models/__init__.py.
  5. Wire up the route in src/libreclient/client.py (both sync and async clients).
  6. Run uv run python scripts/generate_stubs.py to regenerate stubs and __init__ files.
  7. Add tests in tests/unit/routes/test_myroute.py and tests/unit/models/test_myroute.py.

Commit Convention

This project enforces Conventional Commits via commitizen. A git hook validates every commit message automatically.

Format:

type(scope)?: description

[optional body]
[optional footer]

Allowed types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert, bump

See COMMIT_TYPES.md for full definitions and scope conventions.

Examples:

feat(routing): add OSPFv3 port listing
fix: handle empty response from list_devices
docs: add upstream tracking section to README
test: add functional tests for switching routes

Upstream API Tracking

This project tracks which LibreNMS release tag the route implementations are based on. The pinned version is stored in upstream_tracking.toml.

# Check if upstream has a newer release
python scripts/check_upstream.py

# See which API doc files changed
python scripts/check_upstream.py --diff

# See full unified diffs of changed docs
python scripts/check_upstream.py --full

# Compare against a specific tag instead of latest
python scripts/check_upstream.py --diff --tag 26.6.0

# Bump the pinned tag after reviewing changes
python scripts/check_upstream.py --bump

Project Structure

libreclient/
├── src/libreclient/
│   ├── __init__.py            # Public API exports
│   ├── client.py              # LibreClientSync & LibreClientAsync
│   ├── config.py              # Pydantic-settings configuration
│   ├── _base_client.py        # Shared HTTP transport logic
│   ├── models/                # Pydantic response models
│   └── routes/                # Route namespaces (async + sync wrappers)
│       ├── _types.py          # ClientProtocol & utilities
│       ├── _synchronicity.py  # Shared Synchronizer instance
│       ├── alerts.py          # Async route implementation
│       ├── alerts_sync.py     # Sync wrapper
│       └── ...
├── tests/
│   ├── unit/
│   │   ├── models/            # Model validation tests
│   │   └── routes/            # Route logic tests (MockClient)
│   └── functional/            # Live API tests (requires .env)
├── scripts/
│   ├── check_upstream.py      # Detect upstream API doc changes
│   └── generate_stubs.py      # .pyi stub generator
├── .githooks/
│   ├── pre-commit             # Ruff lint & format
│   └── commit-msg             # Conventional commit validation
├── upstream_tracking.toml     # Pinned LibreNMS release tag
├── pyproject.toml
├── CHANGELOG.md
└── LICENSE

Contributing

Contributions are welcome! We're actively looking for contributors to help with:

  • 🐛 Bug fixes and edge case handling
  • ✨ Support for new routes as LibreNMS adds API endpoints
  • 📖 Documentation improvements
  • 🧪 Test coverage expansion
  • 🔧 Tooling and CI improvements

Getting started:

  1. Fork the repo and create a branch from dev
  2. Follow the Development setup below
  3. Make your changes with tests
  4. Open a PR against dev — CI will lint, test, and auto-fix formatting

See CONTRIBUTING.md for detailed guidelines, or just open an issue to discuss your idea first.