Skip to content

Commit fab8402

Browse files
implement centralized environment configuration management
1 parent 738ec0d commit fab8402

15 files changed

Lines changed: 302 additions & 49 deletions

.env.dev

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
ENV=dev
2+
BROWSER=chrome
3+
HEADLESS=false
4+
BROWSER_TIMEOUT=30
5+
IMPLICIT_WAIT=5
6+
EXPLICIT_WAIT=10
7+
PAGE_LOAD_TIMEOUT=30
8+
BASE_URL=https://dev.example.com
9+
API_URL=https://api.dev.example.com
10+
SCREENSHOT_ON_FAILURE=true
11+
LOG_LEVEL=DEBUG
12+
RETRY_COUNT=0
13+
PARALLEL_WORKERS=1
14+
ALLURE_DIR=allure-results

.env.example

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# ──────────────────────────────────────────────
2+
# Selenium Python Framework - Environment Config
3+
# ──────────────────────────────────────────────
4+
# Copy this file: cp .env.example .env.<env>
5+
# Usage: ENV=qa pytest
6+
# ──────────────────────────────────────────────
7+
8+
# Environment: dev | qa | staging | prod
9+
ENV=qa
10+
11+
# ── Browser ───────────────────────────────────
12+
BROWSER=chrome
13+
HEADLESS=false
14+
BROWSER_TIMEOUT=30
15+
IMPLICIT_WAIT=10
16+
EXPLICIT_WAIT=15
17+
PAGE_LOAD_TIMEOUT=30
18+
19+
# ── Application URLs ─────────────────────────
20+
BASE_URL=https://qa.example.com
21+
API_URL=https://api.qa.example.com
22+
23+
# ── WebDriver ─────────────────────────────────
24+
WEBDRIVER_REMOTE_URL=
25+
WEBDRIVER_DOWNLOAD_PATH=
26+
27+
# ── Reporting ─────────────────────────────────
28+
SCREENSHOT_ON_FAILURE=true
29+
LOG_LEVEL=INFO
30+
RETRY_COUNT=0
31+
PARALLEL_WORKERS=1
32+
ALLURE_DIR=allure-results

.env.qa

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
ENV=qa
2+
BROWSER=chrome
3+
HEADLESS=false
4+
BROWSER_TIMEOUT=30
5+
IMPLICIT_WAIT=10
6+
EXPLICIT_WAIT=15
7+
PAGE_LOAD_TIMEOUT=30
8+
BASE_URL=https://qa.example.com
9+
API_URL=https://api.qa.example.com
10+
SCREENSHOT_ON_FAILURE=true
11+
LOG_LEVEL=INFO
12+
RETRY_COUNT=0
13+
PARALLEL_WORKERS=1
14+
ALLURE_DIR=allure-results

.env.staging

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
ENV=staging
2+
BROWSER=chrome
3+
HEADLESS=true
4+
BROWSER_TIMEOUT=30
5+
IMPLICIT_WAIT=15
6+
EXPLICIT_WAIT=20
7+
PAGE_LOAD_TIMEOUT=60
8+
BASE_URL=https://staging.example.com
9+
API_URL=https://api.staging.example.com
10+
SCREENSHOT_ON_FAILURE=true
11+
LOG_LEVEL=INFO
12+
RETRY_COUNT=1
13+
PARALLEL_WORKERS=4
14+
ALLURE_DIR=allure-results

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ results/
3939
docker-compose*.yml
4040
Dockerfile
4141

42-
# Environment
42+
# Environment (local only; .env.dev, .env.qa, .env.staging are tracked)
4343
.env
4444
.env.local
4545
.env.production

api/api_client.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
from .base_api import BaseAPI
2-
from config.environments import ENV_CONFIG
2+
from config.config_loader import load_settings
33

44

55
class APIClient(BaseAPI):
66
_instances: dict = {}
77

8-
def __new__(cls, env: str = "qa"):
8+
def __new__(cls, env: str | None = None):
9+
env = env or "qa"
910
if env not in cls._instances:
1011
instance = super().__new__(cls)
1112
instance._initialized = False
1213
cls._instances[env] = instance
1314
return cls._instances[env]
1415

15-
def __init__(self, env: str = "qa"):
16+
def __init__(self, env: str | None = None):
1617
if not getattr(self, "_initialized", False):
17-
config = ENV_CONFIG.get(env, ENV_CONFIG["qa"])
18-
super().__init__(base_url=config["api_url"])
18+
settings = load_settings(env)
19+
super().__init__(base_url=settings.api_url)
1920
self._initialized = True

config/__init__.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,15 @@
1-
from .environments import ENV_CONFIG
1+
from config.constants import Browser, Environment, Timeout, LogLevel, Directory
2+
from config.settings import Settings
3+
from config.config_loader import load_settings, reload_settings, resolve_env
24

3-
__all__ = ["ENV_CONFIG"]
5+
__all__ = [
6+
"Browser",
7+
"Environment",
8+
"Timeout",
9+
"LogLevel",
10+
"Directory",
11+
"Settings",
12+
"load_settings",
13+
"reload_settings",
14+
"resolve_env",
15+
]

config/config_loader.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import os
2+
from pathlib import Path
3+
from dotenv import load_dotenv
4+
5+
from config.settings import Settings
6+
7+
8+
_CONFIG_CACHE: dict[str, Settings] = {}
9+
10+
_SUPPORTED_ENVS = frozenset({"dev", "qa", "staging", "prod"})
11+
12+
13+
def resolve_env(env: str | None = None) -> str:
14+
env = env or os.getenv("ENV") or "qa"
15+
env = env.lower().strip()
16+
if env not in _SUPPORTED_ENVS:
17+
raise ValueError(
18+
f"Unsupported environment: '{env}'. Must be one of {sorted(_SUPPORTED_ENVS)}"
19+
)
20+
return env
21+
22+
23+
def load_settings(env: str | None = None) -> Settings:
24+
env = resolve_env(env)
25+
26+
if env in _CONFIG_CACHE:
27+
return _CONFIG_CACHE[env]
28+
29+
env_file = Path(f".env.{env}")
30+
31+
if not env_file.exists():
32+
raise FileNotFoundError(
33+
f"Environment file not found: {env_file.resolve()}\n"
34+
f"Create it from .env.example: cp .env.example .env.{env}"
35+
)
36+
37+
load_dotenv(dotenv_path=env_file, override=True)
38+
39+
settings = Settings()
40+
_CONFIG_CACHE[env] = settings
41+
return settings
42+
43+
44+
def reload_settings(env: str | None = None) -> Settings:
45+
env = resolve_env(env)
46+
_CONFIG_CACHE.pop(env, None)
47+
return load_settings(env)

config/constants.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from enum import Enum
2+
3+
4+
class Browser(str, Enum):
5+
CHROME = "chrome"
6+
FIREFOX = "firefox"
7+
EDGE = "edge"
8+
SAFARI = "safari"
9+
10+
11+
class Environment(str, Enum):
12+
DEV = "dev"
13+
QA = "qa"
14+
STAGING = "staging"
15+
PROD = "prod"
16+
17+
18+
class Timeout(float, Enum):
19+
IMPLICIT_WAIT = 10
20+
EXPLICIT_WAIT = 15
21+
PAGE_LOAD = 30
22+
POLL_FREQUENCY = 0.5
23+
24+
25+
class LogLevel(str, Enum):
26+
DEBUG = "DEBUG"
27+
INFO = "INFO"
28+
WARNING = "WARNING"
29+
ERROR = "ERROR"
30+
CRITICAL = "CRITICAL"
31+
32+
33+
class DriverPath(str, Enum):
34+
CHROME = "chrome"
35+
FIREFOX = "gecko"
36+
EDGE = "edge"
37+
38+
39+
class Directory:
40+
REPORTS = "reports"
41+
SCREENSHOTS = "screenshots"
42+
LOGS = "logs"
43+
ALLURE_RESULTS = "allure-results"
44+
ALLURE_REPORT = "allure-report"
45+
DOWNLOADS = "downloads"

config/environments.py

Lines changed: 0 additions & 20 deletions
This file was deleted.

0 commit comments

Comments
 (0)