Repository navigation
Expand file tree
/
Copy pathSettings.py
More file actions
211 lines (177 loc) · 6.64 KB
/
Copy pathSettings.py
File metadata and controls
211 lines (177 loc) · 6.64 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
"""
Dynamic environment configuration loader for the application.
This module provides a robust, auto-initializing configuration system that:
- Detects the project root dynamically by searching for a `.git` directory
or a `.env` file, with a fallback to the current working directory.
- Loads environment variables from `.env.dev` if it exists; otherwise
falls back to `.env`.
- Converts raw environment strings into appropriate Python types
(bools, ints, floats, lists, email strings, and normalized absolute paths).
- Exposes configuration variables globally through the `Settings` class
and a plain `ENV` dictionary for convenience.
Core Components:
- **Settings._find_project_root()**
Locates the correct project root to ensure `.env` discovery works
regardless of where the module is executed.
- **Settings.parse_value()**
Automatically parses environment variable strings into:
* lists (comma-separated)
* booleans
* integers / floats
* emails
* filesystem paths (normalized to absolute paths)
* raw strings (fallback)
- **Settings._load_env()**
Loads environment variables from the selected file, parses each value,
and stores the results in the internal `_env` dictionary. This method
is executed automatically once at import time.
- **Public accessors**
* `Settings.get(key, default)`
* Attribute-style access (`Settings.KEY`)
* Dictionary-style access (`Settings["KEY"]`)
* Full environment access (`Settings.__env__()`)
Initialization:
The environment is loaded immediately during module import, guaranteeing
that all dependent modules have access to parsed configuration values
without requiring explicit initialization calls.
Exports:
- `ENV`: a direct reference to the parsed environment dictionary, suitable
for lightweight imports where class-based access is unnecessary.
Intended Use:
Import `Settings` or `ENV` from this module to access application-wide
configuration values in a safe, typed, and consistent manner.
Dependencies:
- python-dotenv
- re
- pathlib.Path
"""
import re
from pathlib import Path
from dotenv import dotenv_values
class Settings:
"""
Global settings loader that:
- Finds the project root dynamically
- Loads .env.dev if present, else .env
- Parses values into Python types
- Makes all variables globally accessible (Settings.KEY)
- Runs exactly once
"""
_env = {} # Stores parsed variables
ROOT = None # Absolute project root path
ENV_FILE = None # The file actually loaded
# Project Root Detection
@staticmethod
def _find_project_root() -> Path:
"""
Dynamically detect the root folder of the project by looking for
either a .git folder or a .env file. Falls back to cwd.
"""
current = Path(__file__).resolve()
for parent in [current] + list(current.parents):
if (parent / ".git").exists() or (parent / ".env").exists():
return parent
return Path.cwd()
# Value Parser
@staticmethod
def parse_value(value: str):
"""
Convert strings to Python types automatically by detecting the following formats:
1. Parse comma-separated values -> list
2. Booleans -> bool
3. Integers -> number
4. Floats -> number
5. Emails -> string
6. Filepaths -> string
7. Strings -> string
"""
value = value.strip()
# 1. Parse comma-separated arrays (before type-coercing)
if "," in value:
return [v.strip() for v in value.split(",")]
# 2. Booleans
lower = value.lower()
if lower == "true":
return True
if lower == "false":
return False
# 3. Integers
if re.fullmatch(r"-?\d+", value):
return int(value)
# 4. Floats
try:
return float(value)
except ValueError:
pass
# 5. Email detection
email_regex = r"^[\w\.-]+@[\w\.-]+\.\w+$"
if re.match(email_regex, value):
return value
# 6. Paths — convert to absolute paths if path-like
# Detect forward/backslashes OR drive letters
if "/" in value or "\\" in value or re.match(r"^[A-Za-z]:\\", value):
return str((Settings.ROOT / value).resolve())
# Otherwise return raw string
return value
# Loading Environment Variables
@classmethod
def _load_env(cls):
"""Load and parse the .env file into class variable `cls._env`."""
cls.ROOT = cls._find_project_root()
# Determine which .env file to load
dev_path = cls.ROOT / ".env.dev"
default_path = cls.ROOT / ".env"
cls.ENV_FILE = dev_path if dev_path.exists() else default_path
raw_vars = dotenv_values(cls.ENV_FILE)
for key, raw_value in raw_vars.items():
parsed = cls.parse_value(raw_value)
cls._env[key] = parsed
# print()
# Public Accessors
@classmethod
def get(cls, key, default=None):
"""
Retrieve a value from `cls._env[key]`.
:param key: the key of the value to retrieve
:param default: default value to return if `key` is not found
:return: value from `cls._env[key]`
"""
return cls._env.get(key, default)
@classmethod
def __getattr__(cls, key):
"""
Retrieve a value from `cls._env[key]`.
:param key: key of the value to retrieve
:return: value from `cls._env[key]`
"""
if key in cls._env:
return cls._env[key]
raise AttributeError(f"{key} not found in Settings")
@classmethod
def __getitem__(cls, key):
"""
Retrieve a value from `cls._env[key]`.
:param key: key of the value to retrieve
:return: value from `cls._env[key]`
"""
if key in cls._env:
return cls._env[key]
raise KeyError(f"{key} not found in Settings")
@classmethod
def __env__(cls):
"""
Retrieve all environment variables from `cls._env`.
:return: all environment variables from `cls._env`
"""
return cls._env
@property
def env(self):
"""
Retrieve all environment variables from `cls._env`.
:return: all environment variables from `cls._env`
"""
return self._env
# Initialize at import time (runs exactly once)
Settings._load_env()
# Export environment dictionary for easy importing
ENV = Settings.__env__()