Skip to content

feat(config): pydantic-settings module with env-driven runtime configuration#45

Merged
strausmann merged 3 commits into
mainfrom
feat/config-settings
May 11, 2026
Merged

feat(config): pydantic-settings module with env-driven runtime configuration#45
strausmann merged 3 commits into
mainfrom
feat/config-settings

Conversation

@strausmann
Copy link
Copy Markdown
Owner

Summary

Phase 1 Task 1.4 from docs/superpowers/plans/2026-05-11-label-printer-hub.md. The backend now has a typed settings module that reads runtime configuration from PRINTER_HUB_* environment variables (also .env file in dev). Subsequent phases (lookup clients, label renderer, print queue, API webhooks) consume Settings via dependency injection.

What's in this PR

backend/app/config.py

  • Settings(BaseSettings) with env_prefix="PRINTER_HUB_", env_file=".env", extra="ignore".
  • Fields cover database, both Brother printers (host/port), webhook auth key, three integration backends (Snipe-IT, Grocy, Spoolman), and server/log knobs.
  • SecretStr on every credential field — repr/model_dump won't leak secrets in logs.
  • @field_validator("webhook_api_key") enforces 32-char minimum on non-empty values. Empty is accepted so the hub can boot without configured webhook auth (for local dev).
  • @lru_cache def get_settings() for use as a FastAPI dependency.

backend/.env.example

  • All PRINTER_HUB_* fields documented with comments.
  • All URL fields empty — no private HomeLab hostnames/IPs/domains in the public repo.

backend/tests/unit/test_config.py

  • TDD: failing test → implementation → green.
  • test_settings_load_from_env sets 9 env vars and asserts all 9 (no silent-skip on unset assertions — see docs/learnings/code-review-patterns.md "Test assertions must be tight").
  • test_settings_rejects_short_api_key confirms validator behavior.
  • Test values use RFC 5737 documentation addresses (192.0.2.x) so the privacy-scan CI step won't trip on them.

Test plan

  • pytest -q → 38/38 passed (36 existing + 2 new).
  • pytest tests/unit/test_config.py -v → 2/2.
  • ruff check . clean.
  • mypy app/ (strict mode) clean.
  • git grep -nE '(?<![0-9.])192\.168\.[0-9]+\.[0-9]+(?![0-9.])' backend/ → zero matches.

Review history (subagent-driven)

This PR went through the superpowers subagent-driven review loop:

  1. Implementer wrote the initial commit b8f1696.
  2. Spec compliance reviewer: ✅ — all 12 fields, validator, .env.example, tests match the plan.
  3. Code quality reviewer: ❌ — flagged 4 findings (1 critical privacy-scan, 3 important).
  4. Fix commit f24b029 addressing all 4 findings (RFC 5737 addresses, English comments, idiomatic SecretStr("") default, tightened assertions).
  5. Re-review: ✅ APPROVED.

Two minor findings (whitespace-only API-key check, runtime validation on log_level / server_port) were intentionally deferred — they belong in a follow-up since they expand scope beyond plan Task 1.4.

Linked plan

Plan: docs/superpowers/plans/2026-05-11-label-printer-hub.md Task 1.4.

strausmann and others added 2 commits May 11, 2026 07:14
Introduces app/config.py with a PRINTER_HUB_-prefixed BaseSettings class
covering database URL, printer hosts/ports, webhook API key (min 32 chars,
validated), and optional Snipe-IT / Grocy / Spoolman integration fields.
Adds .env.example with documentation comments and a get_settings() helper
cached with lru_cache.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ts, tighter test assertions

- Replace RFC 1918 test addresses (192.168.1.10/11) with RFC 5737
  TEST-NET-1 addresses (192.0.2.10/11) to pass CI privacy-scan
- Translate German section comments in config.py to English
- Drop verbose Field(default=SecretStr("")) in favour of bare SecretStr("")
  for webhook_api_key, snipeit_api_key, grocy_api_key; remove now-unused
  Field import
- Add assertions for all 9 env vars set in test_settings_load_from_env
  (database_url, snipeit_url/api_key, grocy_url/api_key, spoolman_url)
Copilot AI review requested due to automatic review settings May 11, 2026 07:23
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a structured, type-safe configuration system for the backend. By leveraging Pydantic, it enables environment-driven settings management, which simplifies deployment and integration with external services like Snipe-IT, Grocy, and Spoolman. The implementation includes secure handling of credentials and validation logic to ensure system integrity at startup.

Highlights

  • Configuration Management: Implemented a centralized Settings module using pydantic-settings to manage runtime configuration via environment variables with the PRINTER_HUB_ prefix.
  • Security: Utilized SecretStr for sensitive fields to prevent accidental exposure in logs and added a field validator to enforce a 32-character minimum for the webhook API key.
  • Testing: Added comprehensive unit tests to verify environment variable loading and validation logic, ensuring robust configuration handling.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a runtime configuration system using Pydantic Settings, including an example environment file and unit tests to verify environment variable loading and validation. The feedback focuses on enhancing type safety and validation for configuration fields, specifically suggesting the use of Field for port range validation (1-65535) and Literal for restricting logging levels to valid options.

Comment thread backend/app/config.py
Comment on lines +24 to +27
from functools import lru_cache

from pydantic import SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Add Literal and Field to imports to support stricter validation for logging levels and port ranges.

Suggested change
from functools import lru_cache
from pydantic import SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
from functools import lru_cache
from typing import Literal
from pydantic import Field, SecretStr, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

Comment thread backend/app/config.py

# Brother QL-820NWB — address label printer
ql820_host: str = ""
ql820_port: int = 9100
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Add range validation for the printer port to ensure it stays within the valid TCP port range (1-65535).

Suggested change
ql820_port: int = 9100
ql820_port: int = Field(9100, ge=1, le=65535)

Comment thread backend/app/config.py

# Brother PT-750W — cable / panel label printer
pt750w_host: str = ""
pt750w_port: int = 9100
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Add range validation for the printer port to ensure it stays within the valid TCP port range (1-65535).

Suggested change
pt750w_port: int = 9100
pt750w_port: int = Field(9100, ge=1, le=65535)

Comment thread backend/app/config.py
Comment on lines +70 to +71
server_port: int = 8090
log_level: str = "INFO"
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Use Field for port validation and Literal for log_level. This prevents runtime errors from typos in environment variables and improves type safety for the logging configuration.

Suggested change
server_port: int = 8090
log_level: str = "INFO"
server_port: int = Field(8090, ge=1, le=65535)
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] = "INFO"

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a typed runtime configuration layer to the backend using pydantic-settings, enabling environment-driven configuration via the PRINTER_HUB_* prefix (with optional .env support for local dev). This sets the foundation for later phases to consume a single Settings object via dependency injection.

Changes:

  • Introduce app.config.Settings (BaseSettings) with PRINTER_HUB_ env prefix and a cached get_settings() accessor.
  • Add backend/.env.example documenting supported configuration variables.
  • Add unit tests validating env loading and webhook API key minimum-length validation.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 3 comments.

File Description
backend/app/config.py New Pydantic settings module (Settings) plus cached get_settings() accessor and webhook API key validator.
backend/.env.example Example environment file documenting PRINTER_HUB_* variables for local configuration.
backend/tests/unit/test_config.py Unit tests for settings env loading and webhook API key validation.

Comment on lines +20 to +24
monkeypatch.setenv("PRINTER_HUB_SPOOLMAN_URL", "https://spoolman.example")

settings = Settings()

assert settings.database_url == f"sqlite:///{tmp_path}/test.db"
Comment thread backend/tests/unit/test_config.py Outdated
Comment on lines +35 to +38
def test_settings_rejects_short_api_key(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("PRINTER_HUB_WEBHOOK_API_KEY", "too-short")
with pytest.raises(ValueError, match="at least 32"):
Settings()
Comment thread backend/app/config.py Outdated
Comment on lines +16 to +19
settings are only parsed once per process. Tests that instantiate
:class:`Settings` directly bypass the cache and get a fresh read each time —
this is intentional and safe because the two test functions here don't call
:func:`get_settings`.
…m tests

- Collapse multi-line raise ValueError onto one line so ruff format --check passes
- Pass _env_file=None in both Settings() test instantiations to prevent backend/.env
  from leaking into unit tests (hermetic, deterministic)
- Rephrase module docstring to describe the _env_file=None pattern without
  coupling the text to a specific test count or test-function names

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@strausmann strausmann merged commit 878e9e0 into main May 11, 2026
9 checks passed
github-actions Bot pushed a commit that referenced this pull request May 12, 2026
## 0.3.0 (2026-05-12)

* feat(config): pydantic-settings module with env-driven runtime configuration (#45) ([878e9e0](878e9e0)), closes [#45](#45)
* feat(integrations): AppLookupService aggregator — Phase 3 complete (#53) ([222bef4](222bef4)), closes [#53](#53)
* feat(integrations): Grocy + Spoolman lookup clients with shared NotFoundError base (#52) ([b1c9c3c](b1c9c3c)), closes [#52](#52)
* feat(integrations): LabelData schema + Snipe-IT lookup client (#51) ([3bc180f](3bc180f)), closes [#51](#51)
* feat(label-renderer): Template schema + Pillow/qrcode renderer for 1-bit label bitmaps (#54) ([fb77028](fb77028)), closes [#54](#54)
* feat(printer-models): Brother PT-Series TapeRegistry with TZe and heat-shrink specs (#47) ([7526019](7526019)), closes [#47](#47)
* feat(printer-models): Job lifecycle FSM with explicit state machine (#49) ([1a8c40e](1a8c40e)), closes [#49](#49)
* feat(printer-models): PrinterModel Protocol + ModelRegistry for plugin discovery (#48) ([2ae0e09](2ae0e09)), closes [#48](#48)
* feat(printer-models): PrintQueue worker with pause/resume/cancel/retry (#50) ([dfdf6fe](dfdf6fe)), closes [#50](#50)

[skip ci]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants