Skip to content

feat: Split Settings into Database and Operational Subsets - #31

Merged
cschwartz merged 3 commits into
mainfrom
27-split-settings-into-database-and-operational-subsets
Mar 14, 2026
Merged

feat: Split Settings into Database and Operational Subsets#31
cschwartz merged 3 commits into
mainfrom
27-split-settings-into-database-and-operational-subsets

Conversation

@cschwartz

@cschwartz cschwartz commented Mar 14, 2026

Copy link
Copy Markdown
Owner

Implements #27

Summary by CodeRabbit

  • Refactor
    • Separated database-specific configuration into its own settings model with a cached accessor for consistent DB connection handling.
    • Application settings now expose external service URLs (asset inventory, CMDB) and delegate DB info to the new database config.
    • Migration tooling updated to read the database URL from the centralized database settings.

@coderabbitai

coderabbitai Bot commented Mar 14, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a11395f7-6469-4514-ad16-bfbb2f733848

📥 Commits

Reviewing files that changed from the base of the PR and between 08496f7 and f17e56a.

📒 Files selected for processing (1)
  • verdict-backend/app/config.py

📝 Walkthrough

Walkthrough

Extracts database configuration into a new DatabaseSettings model and cached get_database_settings() factory; updates Settings to inherit from DatabaseSettings and removes the old database_url property; Alembic env now imports get_database_settings() and uses db_settings.database_url for migration URLs.

Changes

Cohort / File(s) Summary
Top-level settings
verdict-backend/app/config.py
Settings now inherits from DatabaseSettings (moved from BaseSettings). Database-specific fields and the old database_url property were removed. Adds/keeps cached get_settings() and module-level settings = get_settings().
New database config module
verdict-backend/app/database_config.py
Adds DatabaseSettings pydantic model with host/port/name/user/password and database_url property. Adds cached get_database_settings() factory returning a DatabaseSettings instance.
Alembic env update
verdict-backend/alembic/env.py
Replaces get_settings import with get_database_settings; initializes db_settings = get_database_settings() and uses db_settings.database_url in both offline and online Alembic flows.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 I hopped through configs, tidy and bright,
Pulled DB bits out into the light.
A cached little getter, neat and fast,
Alembic finds its URL at last.
Nibble, bounce, commit — changes done with delight! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and accurately summarizes the main objective: splitting Settings into database and operational subsets, which is the primary purpose of all changes across the modified files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 27-split-settings-into-database-and-operational-subsets
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@verdict-backend/app/config.py`:
- Around line 39-41: The module currently instantiates DatabaseSettings eagerly
when get_database_settings is imported; change get_database_settings to return a
lazy proxy that defers creating the real DatabaseSettings until the first
attribute access. Implement a small LazyDatabaseSettings inside
get_database_settings (or nearby) that accepts a factory (lambda:
DatabaseSettings()), implements __getattr__ to instantiate and cache the real
DatabaseSettings on first access and then delegate attribute lookups, keep the
`@lru_cache` on get_database_settings, and return the LazyDatabaseSettings
instance so existing call sites using settings.foo continue to work without
triggering full Settings initialization at import time.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5b71b4df-c70a-4432-bc48-d2c35567ad77

📥 Commits

Reviewing files that changed from the base of the PR and between df2b999 and c929b15.

📒 Files selected for processing (2)
  • verdict-backend/alembic/env.py
  • verdict-backend/app/config.py

Comment thread verdict-backend/app/config.py Outdated
Comment on lines +39 to +41
@lru_cache
def get_database_settings() -> DatabaseSettings:
return DatabaseSettings()

@coderabbitai coderabbitai Bot Mar 14, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

get_database_settings() is still blocked by eager full-settings initialization.

Importing get_database_settings still executes Line 50, so app.config eagerly builds Settings() and requires asset_inventory_url/cmdb_url up front. In verdict-backend/alembic/env.py Line 10, that means Alembic can still fail during import before get_database_settings() is ever used, which defeats the DB-only split.

Minimal fix that preserves existing `settings.foo` call sites
 `@lru_cache`
 def get_settings() -> Settings:
     """Cached settings instance"""
     return Settings()
 
-settings = get_settings()
+class _SettingsProxy:
+    def __getattr__(self, name: str):
+        return getattr(get_settings(), name)
+
+settings = _SettingsProxy()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@verdict-backend/app/config.py` around lines 39 - 41, The module currently
instantiates DatabaseSettings eagerly when get_database_settings is imported;
change get_database_settings to return a lazy proxy that defers creating the
real DatabaseSettings until the first attribute access. Implement a small
LazyDatabaseSettings inside get_database_settings (or nearby) that accepts a
factory (lambda: DatabaseSettings()), implements __getattr__ to instantiate and
cache the real DatabaseSettings on first access and then delegate attribute
lookups, keep the `@lru_cache` on get_database_settings, and return the
LazyDatabaseSettings instance so existing call sites using settings.foo continue
to work without triggering full Settings initialization at import time.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

This is incorrect. get_settings is executed on line 50 and fails if the env vars are not available. get_database_settings is only executed within function bodies run_migrations_offline / run_migrations_online.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

Let me check the alembic/env.py file to verify the exact import and call patterns before responding.


🏁 Script executed:

#!/bin/bash
cat verdict-backend/alembic/env.py

Length of output: 2168


@cschwartz, you're right that get_database_settings() itself is only invoked inside the run_migrations_offline/run_migrations_online function bodies and is not eagerly called — that part of my original comment was incorrectly framed.

The residual concern is narrower: because from app.config import get_database_settings causes Python to execute the entire app/config.py module on first import, the module-level statement settings = get_settings() (line 50) will still fire at that point, which instantiates the full Settings class and requires asset_inventory_url/cmdb_url to be present. If the intent of the DB/operational split is to allow an Alembic-only environment to run with only the database env vars set, line 50 would still prevent that.

If that is not the goal of this PR (i.e., all env vars are expected to be present in every runtime context, and the split is purely a code-organisation concern), then your design is correct and my original comment was indeed off-base. Happy to close this out if that's the case.


✏️ Learnings added
Learnt from: cschwartz
Repo: cschwartz/verdict PR: 31
File: verdict-backend/app/config.py:39-41
Timestamp: 2026-03-14T14:51:09.345Z
Learning: In `verdict-backend/alembic/env.py`, `get_database_settings()` is imported from `app.config` at the module level but is only *called* inside the `run_migrations_offline()` and `run_migrations_online()` function bodies — it is not eagerly invoked. The module-level `settings = get_settings()` in `verdict-backend/app/config.py` (line 50) is a separate concern that fires on first import of the module.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
verdict-backend/app/database_config.py (1)

21-27: Special characters in credentials may break the URL.

If database_user, database_password, or database_name contain characters like @, :, /, or %, the constructed URL will be malformed or misinterpreted by SQLAlchemy.

♻️ Proposed fix using urllib.parse.quote_plus
 from functools import lru_cache
+from urllib.parse import quote_plus
 
 from pydantic import Field, SecretStr
 from pydantic_settings import BaseSettings, SettingsConfigDict
     `@property`
     def database_url(self) -> str:
+        user = quote_plus(self.database_user)
+        password = quote_plus(self.database_password.get_secret_value())
+        name = quote_plus(self.database_name)
         return (
             f"postgresql+psycopg2://"
-            f"{self.database_user}:{self.database_password.get_secret_value()}"
+            f"{user}:{password}"
-            f"@{self.database_host}:{self.database_port}/{self.database_name}"
+            f"@{self.database_host}:{self.database_port}/{name}"
         )
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@verdict-backend/app/database_config.py` around lines 21 - 27, The constructed
database_url in the database_url property can be broken by special characters in
database_user, database_password.get_secret_value(), or database_name; update
database_url to percent-encode these components using urllib.parse.quote_plus
(import it at top) and apply quote_plus to the username, password (use the
secret value), and database name before formatting the f-string so the returned
URL is safe for SQLAlchemy to parse.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@verdict-backend/app/config.py`:
- Around line 16-18: Remove the duplicate cached DatabaseSettings provider in
config.py: delete the local get_database_settings() definition and instead
import and re-export the single source of truth from database_config (e.g. from
database_config import get_database_settings); this ensures only one `@lru_cache`
is used across the app and avoids separate caches for the same settings.

---

Nitpick comments:
In `@verdict-backend/app/database_config.py`:
- Around line 21-27: The constructed database_url in the database_url property
can be broken by special characters in database_user,
database_password.get_secret_value(), or database_name; update database_url to
percent-encode these components using urllib.parse.quote_plus (import it at top)
and apply quote_plus to the username, password (use the secret value), and
database name before formatting the f-string so the returned URL is safe for
SQLAlchemy to parse.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8607e857-d7f1-4692-8733-999988667e2a

📥 Commits

Reviewing files that changed from the base of the PR and between c929b15 and 08496f7.

📒 Files selected for processing (3)
  • verdict-backend/alembic/env.py
  • verdict-backend/app/config.py
  • verdict-backend/app/database_config.py

Comment thread verdict-backend/app/config.py Outdated
@cschwartz
cschwartz merged commit 60d05e8 into main Mar 14, 2026
1 of 3 checks passed
@cschwartz
cschwartz deleted the 27-split-settings-into-database-and-operational-subsets branch March 14, 2026 17:57
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.

1 participant