-
Notifications
You must be signed in to change notification settings - Fork 0
Unify environment handling in Python #55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| """Unified environment parsing and resolution utilities. | ||
|
|
||
| This module centralizes how the application determines its deployment | ||
| environment to avoid duplication and ambiguity across the codebase. | ||
|
|
||
| Canonical environments: dev | beta | prod | ||
|
|
||
| Accepted synonyms (case-insensitive): | ||
| - development -> dev | ||
| - production -> prod | ||
|
|
||
| Priority for resolution (highest to lowest): | ||
| 1) CONFIG_FILE (extracts env from filename like config/prod.yaml) | ||
| 2) ENV | ||
| 3) ENVIRONMENT | ||
| 4) default: dev | ||
| """ | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| import os | ||
| import re | ||
| from typing import Optional | ||
|
|
||
| from .models import Environment, EnvironmentInfo | ||
|
|
||
|
|
||
| def parse_environment_str(value: Optional[str]) -> Environment: | ||
| """Parse a string into the canonical Environment enum. | ||
|
|
||
| Accepts canonical values (dev|beta|prod) and common synonyms | ||
| like development|production. | ||
| """ | ||
| if not value: | ||
| return Environment.DEV | ||
|
|
||
| s = value.strip().lower() | ||
| # Map synonyms | ||
| if s == "development": | ||
| s = "dev" | ||
| elif s == "production": | ||
| s = "prod" | ||
|
|
||
| try: | ||
| return Environment(s) | ||
| except ValueError: | ||
| # Default safely to DEV and let callers decide if they want to warn | ||
| return Environment.DEV | ||
|
|
||
|
|
||
| def resolve_environment(config_file: Optional[str] = None) -> Environment: | ||
| """Resolve the current environment using a consistent precedence order. | ||
|
|
||
| Precedence: | ||
| 1) CONFIG_FILE (env extracted from its basename, e.g., beta.yaml) | ||
| 2) ENV | ||
| 3) ENVIRONMENT | ||
| 4) default to dev | ||
| """ | ||
| # 1) CONFIG_FILE (argument overrides env var for testability) | ||
| config_file = config_file or os.getenv("CONFIG_FILE") | ||
| if config_file: | ||
| # Try to extract the environment from the filename | ||
| # e.g., /app/config/beta.yaml -> beta | ||
| m = re.search(r"/(\\w+)\\.yaml$", config_file) | ||
| if m: | ||
| return parse_environment_str(m.group(1)) | ||
|
|
||
| # 2) ENV | ||
| env = os.getenv("ENV") | ||
| if env: | ||
| return parse_environment_str(env) | ||
|
|
||
| # 3) ENVIRONMENT (supports development/production synonyms) | ||
| env2 = os.getenv("ENVIRONMENT") | ||
| if env2: | ||
| return parse_environment_str(env2) | ||
|
|
||
| # 4) fallback | ||
| return Environment.DEV | ||
|
|
||
|
|
||
| def get_environment_info() -> EnvironmentInfo: | ||
| """Return an EnvironmentInfo snapshot using the unified resolver.""" | ||
| env = resolve_environment() | ||
| return EnvironmentInfo( | ||
| name=env, | ||
| is_production=(env == Environment.PROD), | ||
| is_development=(env == Environment.DEV), | ||
| ) | ||
|
|
||
|
|
||
| def environment_name() -> str: | ||
| """Convenience accessor for the current environment name as a string.""" | ||
| return resolve_environment().value | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -10,19 +10,13 @@ | |||||
|
|
||||||
| import os | ||||||
| import yaml | ||||||
| from enum import Enum | ||||||
| from pathlib import Path | ||||||
| from typing import Dict, Any, Optional | ||||||
| from dotenv import load_dotenv | ||||||
|
|
||||||
| from .config import ServerConfig, DevelopmentConfig, ProductionConfig | ||||||
|
|
||||||
|
|
||||||
| class Environment(Enum): | ||||||
| """Supported environments.""" | ||||||
| DEV = "dev" | ||||||
| BETA = "beta" | ||||||
| PROD = "prod" | ||||||
| from ..common.models import Environment | ||||||
| from ..common.environment import resolve_environment | ||||||
|
|
||||||
|
|
||||||
| class ConfigLoader: | ||||||
|
|
@@ -190,26 +184,12 @@ def get_config(self, environment: Optional[str] = None, force_reload: bool = Fal | |||||
| # Load environment variables first | ||||||
| self.load_environment_variables() | ||||||
|
|
||||||
| # Determine and validate environment | ||||||
| # Determine and validate environment using unified resolver | ||||||
| if environment is None: | ||||||
| # Check CONFIG_FILE first, then ENV, then ENVIRONMENT, then default to dev | ||||||
| config_file = os.getenv("CONFIG_FILE") | ||||||
| if config_file: | ||||||
| # Extract environment from config file path (e.g., /app/config/beta.yaml -> beta) | ||||||
| import re | ||||||
| match = re.search(r'/(\w+)\.yaml$', config_file) | ||||||
| if match: | ||||||
| environment = match.group(1) | ||||||
| else: | ||||||
| environment = "dev" | ||||||
| else: | ||||||
| environment = os.getenv("ENV", os.getenv("ENVIRONMENT", "dev")) | ||||||
|
|
||||||
| try: | ||||||
| env_enum = resolve_environment() | ||||||
| else: | ||||||
| env_enum = Environment(environment) | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When an You should use the
Suggested change
|
||||||
| except ValueError: | ||||||
| supported_envs = [e.value for e in Environment] | ||||||
| raise ValueError(f"Unsupported environment '{environment}'. Supported environments: {supported_envs}") | ||||||
|
|
||||||
|
|
||||||
| # Load YAML config | ||||||
| yaml_config = self.load_yaml_config(env_enum) | ||||||
|
|
@@ -253,4 +233,4 @@ def get_config(environment: Optional[str] = None) -> ServerConfig: | |||||
| def reset_config(): | ||||||
| """Reset the global config loader (for testing).""" | ||||||
| global _config_loader | ||||||
| _config_loader = None | ||||||
| _config_loader = None | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The regular expression used to extract the environment from the config filename is incorrect and not robust. The double backslashes
\\in the raw string will be treated as literal backslashes, which will cause the pattern to fail to match. Additionally, using a regex that matches on the full path is brittle as it assumes a specific directory structure.A more robust approach is to operate on the base name of the file. An even better, more Pythonic solution would be to use
pathlib.Path(config_file).stemto avoid regex altogether.