feat(config): pydantic-settings module with env-driven runtime configuration#45
Conversation
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)
Summary of ChangesHello, 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
Using Gemini Code AssistThe 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
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 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
|
There was a problem hiding this comment.
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.
| from functools import lru_cache | ||
|
|
||
| from pydantic import SecretStr, field_validator | ||
| from pydantic_settings import BaseSettings, SettingsConfigDict |
There was a problem hiding this comment.
Add Literal and Field to imports to support stricter validation for logging levels and port ranges.
| 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 |
|
|
||
| # Brother QL-820NWB — address label printer | ||
| ql820_host: str = "" | ||
| ql820_port: int = 9100 |
|
|
||
| # Brother PT-750W — cable / panel label printer | ||
| pt750w_host: str = "" | ||
| pt750w_port: int = 9100 |
| server_port: int = 8090 | ||
| log_level: str = "INFO" |
There was a problem hiding this comment.
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.
| 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" |
There was a problem hiding this comment.
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) withPRINTER_HUB_env prefix and a cachedget_settings()accessor. - Add
backend/.env.exampledocumenting 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. |
| monkeypatch.setenv("PRINTER_HUB_SPOOLMAN_URL", "https://spoolman.example") | ||
|
|
||
| settings = Settings() | ||
|
|
||
| assert settings.database_url == f"sqlite:///{tmp_path}/test.db" |
| 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() |
| 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>
## 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]
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 fromPRINTER_HUB_*environment variables (also.envfile in dev). Subsequent phases (lookup clients, label renderer, print queue, API webhooks) consumeSettingsvia dependency injection.What's in this PR
backend/app/config.pySettings(BaseSettings)withenv_prefix="PRINTER_HUB_",env_file=".env",extra="ignore".SecretStron every credential field —repr/model_dumpwon'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.examplePRINTER_HUB_*fields documented with comments.backend/tests/unit/test_config.pytest_settings_load_from_envsets 9 env vars and asserts all 9 (no silent-skip on unset assertions — seedocs/learnings/code-review-patterns.md"Test assertions must be tight").test_settings_rejects_short_api_keyconfirms validator behavior.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:
b8f1696..env.example, tests match the plan.f24b029addressing all 4 findings (RFC 5737 addresses, English comments, idiomaticSecretStr("")default, tightened assertions).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.mdTask 1.4.