ga-configreader 0.1.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- configreader/__init__.py +8 -0
- configreader/_version.py +1 -0
- configreader/configreader.py +481 -0
- configreader/py.typed +1 -0
- ga_configreader-0.1.3.dist-info/METADATA +239 -0
- ga_configreader-0.1.3.dist-info/RECORD +9 -0
- ga_configreader-0.1.3.dist-info/WHEEL +5 -0
- ga_configreader-0.1.3.dist-info/licenses/LICENSE +21 -0
- ga_configreader-0.1.3.dist-info/top_level.txt +1 -0
configreader/__init__.py
ADDED
configreader/_version.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.3"
|
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import ast
|
|
5
|
+
import configparser
|
|
6
|
+
from enum import Enum
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
from sqlalchemy import create_engine
|
|
12
|
+
from sqlalchemy import inspect
|
|
13
|
+
from sqlalchemy import text
|
|
14
|
+
from sqlalchemy.orm import sessionmaker
|
|
15
|
+
from sqlalchemy.exc import SQLAlchemyError
|
|
16
|
+
except Exception:
|
|
17
|
+
create_engine = None
|
|
18
|
+
inspect = None
|
|
19
|
+
text = None
|
|
20
|
+
sessionmaker = None
|
|
21
|
+
SQLAlchemyError = Exception
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConfigSource(Enum):
|
|
25
|
+
"""Supported configuration providers.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
INI: Read values from an INI file.
|
|
29
|
+
DB: Read values from a database using SQLAlchemy.
|
|
30
|
+
ENV: Read values from environment variables.
|
|
31
|
+
DICT: Read values from an in-memory dictionary.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
INI = "ini"
|
|
35
|
+
DB = "db"
|
|
36
|
+
ENV = "env"
|
|
37
|
+
DICT = "dict"
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def parse(cls, value: str) -> ConfigSource | None:
|
|
41
|
+
"""Parse a provider string into a ConfigSource enum value.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
value: Provider name (e.g. "ini", "db", "env", "dict").
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The matching ConfigSource member, or None if unsupported.
|
|
48
|
+
|
|
49
|
+
Raises:
|
|
50
|
+
None.
|
|
51
|
+
"""
|
|
52
|
+
for item in cls:
|
|
53
|
+
if item.value == value:
|
|
54
|
+
return item
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
def __str__(self) -> str:
|
|
58
|
+
"""Return the provider name as a string.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
The provider value (e.g. "ini", "db", "env", "dict").
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
None.
|
|
65
|
+
"""
|
|
66
|
+
return self.value
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ConfigReader:
|
|
70
|
+
"""Read configuration values from multiple providers with fallback order.
|
|
71
|
+
|
|
72
|
+
The reader checks providers in order and returns the first non-None value.
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
file: str | Path | None = None,
|
|
78
|
+
dictionary: dict[str, dict[str, str]] | None = None,
|
|
79
|
+
db_url: str | None = None,
|
|
80
|
+
db_query: str | None = None,
|
|
81
|
+
use_env: bool = True,
|
|
82
|
+
providers: list[ConfigSource | str] | None = None,
|
|
83
|
+
):
|
|
84
|
+
"""Initialize the reader with one or more configuration providers.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
file: Optional path to an INI file.
|
|
88
|
+
dictionary: Optional nested dictionary grouped by section and key.
|
|
89
|
+
db_url: Optional SQLAlchemy database URL.
|
|
90
|
+
db_query: Optional SQL query with :section and :name bind parameters.
|
|
91
|
+
use_env: Enable or disable environment variable lookup.
|
|
92
|
+
providers: Provider priority order. Accepts ConfigSource values or strings.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
FileNotFoundError: If file is provided and does not exist.
|
|
96
|
+
ImportError: If DB provider is enabled and SQLAlchemy is unavailable.
|
|
97
|
+
"""
|
|
98
|
+
self.config = configparser.ConfigParser()
|
|
99
|
+
|
|
100
|
+
# File .ini
|
|
101
|
+
self.use_ini = file is not None
|
|
102
|
+
|
|
103
|
+
if self.use_ini:
|
|
104
|
+
file_name: str = str(file)
|
|
105
|
+
if os.path.exists(file_name):
|
|
106
|
+
self.config.read(file_name)
|
|
107
|
+
else:
|
|
108
|
+
raise FileNotFoundError(f"INI file '{file_name}' does not exist")
|
|
109
|
+
|
|
110
|
+
self.use_db = db_url is not None
|
|
111
|
+
self.db_url = db_url
|
|
112
|
+
self.db_query = db_query or "SELECT value FROM settings WHERE section = :section AND name = :name"
|
|
113
|
+
self.db_session = None
|
|
114
|
+
|
|
115
|
+
self.use_env = use_env
|
|
116
|
+
|
|
117
|
+
self.use_dict = dictionary is not None
|
|
118
|
+
self.dictionary = dictionary
|
|
119
|
+
|
|
120
|
+
if self.use_db and self.db_url:
|
|
121
|
+
self._init_db()
|
|
122
|
+
|
|
123
|
+
# Keep provider ordering explicit and stable while avoiding mutable defaults.
|
|
124
|
+
if providers is None:
|
|
125
|
+
providers = [ConfigSource.INI, ConfigSource.DB, ConfigSource.ENV, ConfigSource.DICT]
|
|
126
|
+
self.order = [p if isinstance(p, ConfigSource) else ConfigSource.parse(p) for p in providers]
|
|
127
|
+
|
|
128
|
+
def items(self):
|
|
129
|
+
"""Iterate over all entries loaded from the INI provider.
|
|
130
|
+
|
|
131
|
+
Yields:
|
|
132
|
+
Tuples in the form (section, name, value).
|
|
133
|
+
|
|
134
|
+
Raises:
|
|
135
|
+
None.
|
|
136
|
+
"""
|
|
137
|
+
for sec in self.config.sections():
|
|
138
|
+
for name, value in self.config.items(sec):
|
|
139
|
+
yield sec, name, value
|
|
140
|
+
|
|
141
|
+
def _init_db(self):
|
|
142
|
+
"""Create and store a SQLAlchemy session for DB lookups.
|
|
143
|
+
|
|
144
|
+
Raises:
|
|
145
|
+
ImportError: If SQLAlchemy is not installed.
|
|
146
|
+
ValueError: If db_url is missing.
|
|
147
|
+
|
|
148
|
+
Notes:
|
|
149
|
+
SQLAlchemy runtime connection errors are caught and logged, and do not
|
|
150
|
+
raise further exceptions from this method.
|
|
151
|
+
"""
|
|
152
|
+
if create_engine is None or sessionmaker is None:
|
|
153
|
+
raise ImportError("SQLAlchemy is not available")
|
|
154
|
+
if not self.db_url:
|
|
155
|
+
raise ValueError("Database URL is not provided")
|
|
156
|
+
try:
|
|
157
|
+
engine = create_engine(self.db_url)
|
|
158
|
+
Session = sessionmaker(bind=engine)
|
|
159
|
+
self.db_session = Session()
|
|
160
|
+
except SQLAlchemyError as ex:
|
|
161
|
+
print(f"Error initializing database connection: {ex}")
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def check_db_connection(db_url: str) -> bool:
|
|
165
|
+
"""Check whether a DB connection can be established for a URL.
|
|
166
|
+
|
|
167
|
+
Args:
|
|
168
|
+
db_url: SQLAlchemy connection URL.
|
|
169
|
+
|
|
170
|
+
Returns:
|
|
171
|
+
True if a connection can be opened, False otherwise.
|
|
172
|
+
|
|
173
|
+
Raises:
|
|
174
|
+
None.
|
|
175
|
+
"""
|
|
176
|
+
if create_engine is None:
|
|
177
|
+
return False
|
|
178
|
+
try:
|
|
179
|
+
engine = create_engine(db_url)
|
|
180
|
+
with engine.connect() as connection:
|
|
181
|
+
connection.close()
|
|
182
|
+
return True
|
|
183
|
+
except SQLAlchemyError:
|
|
184
|
+
return False
|
|
185
|
+
|
|
186
|
+
@staticmethod
|
|
187
|
+
def check_db_exists(db_url: str, table_name: str = "settings") -> bool:
|
|
188
|
+
"""Check whether a table exists in the target database.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
db_url: SQLAlchemy connection URL.
|
|
192
|
+
table_name: Table to check for existence.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
True if the table exists, False otherwise.
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
None.
|
|
199
|
+
"""
|
|
200
|
+
if create_engine is None or inspect is None:
|
|
201
|
+
return False
|
|
202
|
+
try:
|
|
203
|
+
engine = create_engine(db_url)
|
|
204
|
+
inspector = inspect(engine)
|
|
205
|
+
return inspector.has_table(table_name)
|
|
206
|
+
except SQLAlchemyError:
|
|
207
|
+
return False
|
|
208
|
+
|
|
209
|
+
def _get_from_dict(self, section: str, name: str) -> str | None:
|
|
210
|
+
"""Read a value from the dictionary provider.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
section: Configuration section.
|
|
214
|
+
name: Configuration key.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Value converted to string if found, otherwise None.
|
|
218
|
+
|
|
219
|
+
Raises:
|
|
220
|
+
None.
|
|
221
|
+
"""
|
|
222
|
+
if not self.use_dict:
|
|
223
|
+
return None
|
|
224
|
+
if not self.dictionary:
|
|
225
|
+
return None
|
|
226
|
+
value = self.dictionary.get(section, {}).get(name)
|
|
227
|
+
return str(value) if value is not None else None
|
|
228
|
+
|
|
229
|
+
def _get_from_db(self, section: str, name: str) -> str | None:
|
|
230
|
+
"""Read a value from the database provider.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
section: Configuration section.
|
|
234
|
+
name: Configuration key.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Database value as string if found, otherwise None.
|
|
238
|
+
|
|
239
|
+
Raises:
|
|
240
|
+
None.
|
|
241
|
+
|
|
242
|
+
Notes:
|
|
243
|
+
SQLAlchemy runtime errors are caught and logged.
|
|
244
|
+
"""
|
|
245
|
+
if not self.use_db:
|
|
246
|
+
return None
|
|
247
|
+
if not self.db_session:
|
|
248
|
+
return None
|
|
249
|
+
if text is None:
|
|
250
|
+
return None
|
|
251
|
+
try:
|
|
252
|
+
statement = text(self.db_query)
|
|
253
|
+
result = self.db_session.execute(statement, {"name": name, "section": section}).scalar_one_or_none()
|
|
254
|
+
return result if isinstance(result, str) else (str(result) if result is not None else None)
|
|
255
|
+
except SQLAlchemyError as ex:
|
|
256
|
+
print(f"Error fetching {name} from database: {ex}")
|
|
257
|
+
return None
|
|
258
|
+
|
|
259
|
+
def _get_from_ini(self, section: str, name: str) -> str | None:
|
|
260
|
+
"""Read a value from the INI provider using ConfigParser fallback.
|
|
261
|
+
|
|
262
|
+
Args:
|
|
263
|
+
section: Configuration section.
|
|
264
|
+
name: Configuration key.
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
INI value if found, otherwise None.
|
|
268
|
+
|
|
269
|
+
Raises:
|
|
270
|
+
None.
|
|
271
|
+
"""
|
|
272
|
+
if not self.use_ini:
|
|
273
|
+
return None
|
|
274
|
+
return self.config.get(section, name, fallback=None)
|
|
275
|
+
|
|
276
|
+
def _get_from_env(self, section: str, name: str) -> str | None:
|
|
277
|
+
"""Read a value from environment variables.
|
|
278
|
+
|
|
279
|
+
Naming convention:
|
|
280
|
+
- DEFAULT section: NAME
|
|
281
|
+
- Custom section: SECTION_NAME
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
section: Configuration section.
|
|
285
|
+
name: Configuration key.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
Environment variable value if found, otherwise None.
|
|
289
|
+
|
|
290
|
+
Raises:
|
|
291
|
+
None.
|
|
292
|
+
"""
|
|
293
|
+
if not self.use_env:
|
|
294
|
+
return None
|
|
295
|
+
# DEFAULT uses NAME; custom sections use SECTION_NAME.
|
|
296
|
+
if section.upper() == "DEFAULT":
|
|
297
|
+
return os.getenv(name.upper())
|
|
298
|
+
return os.getenv(f"{section}_{name}".upper())
|
|
299
|
+
|
|
300
|
+
def get(self, name: str, default: str | None = None, section: str = "DEFAULT") -> str | None:
|
|
301
|
+
"""Resolve a configuration value using provider priority order.
|
|
302
|
+
|
|
303
|
+
Args:
|
|
304
|
+
name: Key name inside the section.
|
|
305
|
+
default: Value returned when no provider has a value.
|
|
306
|
+
section: Configuration section.
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
The first resolved value as a string, otherwise default.
|
|
310
|
+
|
|
311
|
+
Raises:
|
|
312
|
+
None.
|
|
313
|
+
"""
|
|
314
|
+
value = None
|
|
315
|
+
# Providers are queried in order; first non-None wins.
|
|
316
|
+
for provider in self.order:
|
|
317
|
+
if provider == ConfigSource.INI:
|
|
318
|
+
value = self._get_from_ini(section, name)
|
|
319
|
+
elif provider == ConfigSource.DB:
|
|
320
|
+
value = self._get_from_db(section, name)
|
|
321
|
+
elif provider == ConfigSource.ENV:
|
|
322
|
+
value = self._get_from_env(section, name)
|
|
323
|
+
elif provider == ConfigSource.DICT:
|
|
324
|
+
value = self._get_from_dict(section, name)
|
|
325
|
+
|
|
326
|
+
if value is not None:
|
|
327
|
+
break
|
|
328
|
+
|
|
329
|
+
return value if value is not None else default
|
|
330
|
+
|
|
331
|
+
def getint(self, name: str, default: int | None = None, section: str = "DEFAULT") -> int | None:
|
|
332
|
+
"""Get a value and convert it to int.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
name: Configuration key.
|
|
336
|
+
default: Returned when no value is found.
|
|
337
|
+
section: Configuration section.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Parsed integer value, or default if unresolved.
|
|
341
|
+
|
|
342
|
+
Raises:
|
|
343
|
+
ValueError: If the resolved value cannot be converted to int.
|
|
344
|
+
TypeError: If the resolved value type is not compatible with int().
|
|
345
|
+
"""
|
|
346
|
+
value = self.get(name, section=section, default=None)
|
|
347
|
+
return int(value) if value is not None else default
|
|
348
|
+
|
|
349
|
+
def getboolean(self, name: str, default: bool | None = None, section: str = "DEFAULT") -> bool | None:
|
|
350
|
+
"""Get a value and convert it to bool.
|
|
351
|
+
|
|
352
|
+
Truthy values are: "true", "1", "yes" (case-insensitive).
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
name: Configuration key.
|
|
356
|
+
default: Returned when no value is found.
|
|
357
|
+
section: Configuration section.
|
|
358
|
+
|
|
359
|
+
Returns:
|
|
360
|
+
True for truthy strings, False for other resolved strings,
|
|
361
|
+
or default if unresolved.
|
|
362
|
+
|
|
363
|
+
Raises:
|
|
364
|
+
None.
|
|
365
|
+
"""
|
|
366
|
+
value = self.get(name, section=section, default=None)
|
|
367
|
+
return value.lower() in ("true", "1", "yes") if value is not None else default
|
|
368
|
+
|
|
369
|
+
def getfloat(self, name: str, default: float | None = None, section: str = "DEFAULT") -> float | None:
|
|
370
|
+
"""Get a value and convert it to float.
|
|
371
|
+
|
|
372
|
+
Args:
|
|
373
|
+
name: Configuration key.
|
|
374
|
+
default: Returned when no value is found.
|
|
375
|
+
section: Configuration section.
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
Parsed float value, or default if unresolved.
|
|
379
|
+
|
|
380
|
+
Raises:
|
|
381
|
+
ValueError: If the resolved value cannot be converted to float.
|
|
382
|
+
TypeError: If the resolved value type is not compatible with float().
|
|
383
|
+
"""
|
|
384
|
+
value = self.get(name, section=section, default=None)
|
|
385
|
+
return float(value) if value is not None else default
|
|
386
|
+
|
|
387
|
+
def getlist(self, name: str, default: list[Any] | None = None, section: str = "DEFAULT") -> list[Any] | None:
|
|
388
|
+
"""Get a value and parse it as a Python list literal.
|
|
389
|
+
|
|
390
|
+
Args:
|
|
391
|
+
name: Configuration key.
|
|
392
|
+
default: Returned when no value is found.
|
|
393
|
+
section: Configuration section.
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
Parsed Python object from literal_eval, or default if unresolved.
|
|
397
|
+
|
|
398
|
+
Raises:
|
|
399
|
+
ValueError: If the value contains a malformed literal.
|
|
400
|
+
SyntaxError: If the value is not valid Python literal syntax.
|
|
401
|
+
MemoryError: In rare cases for extremely large literals.
|
|
402
|
+
"""
|
|
403
|
+
value = self.get(name, section=section, default=None)
|
|
404
|
+
# Complex values are parsed from string literals.
|
|
405
|
+
return ast.literal_eval(value) if value is not None else default
|
|
406
|
+
|
|
407
|
+
def getset(self, name: str, default: set[Any] | None = None, section: str = "DEFAULT") -> set[Any] | None:
|
|
408
|
+
"""Get a value and parse it as a set.
|
|
409
|
+
|
|
410
|
+
If the resolved value is already a set, it is returned as-is.
|
|
411
|
+
|
|
412
|
+
Args:
|
|
413
|
+
name: Configuration key.
|
|
414
|
+
default: Returned when no value is found.
|
|
415
|
+
section: Configuration section.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
Parsed set value, or default if unresolved.
|
|
419
|
+
|
|
420
|
+
Raises:
|
|
421
|
+
ValueError: If the value contains a malformed literal.
|
|
422
|
+
SyntaxError: If the value is not valid Python literal syntax.
|
|
423
|
+
TypeError: If the parsed value is not iterable for set conversion.
|
|
424
|
+
"""
|
|
425
|
+
value = self.get(name, section=section, default=None)
|
|
426
|
+
if isinstance(value, set):
|
|
427
|
+
return value
|
|
428
|
+
return set(ast.literal_eval(value)) if value is not None else default
|
|
429
|
+
|
|
430
|
+
def gettuple(
|
|
431
|
+
self, name: str, section: str = "DEFAULT", default: tuple[Any, ...] | None = None
|
|
432
|
+
) -> tuple[Any, ...] | None:
|
|
433
|
+
"""Get a value and parse it as a tuple.
|
|
434
|
+
|
|
435
|
+
If the resolved value is already a tuple, it is returned as-is.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
name: Configuration key.
|
|
439
|
+
section: Configuration section.
|
|
440
|
+
default: Returned when no value is found.
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
Parsed tuple value, or default if unresolved.
|
|
444
|
+
|
|
445
|
+
Raises:
|
|
446
|
+
ValueError: If the value contains a malformed literal.
|
|
447
|
+
SyntaxError: If the value is not valid Python literal syntax.
|
|
448
|
+
TypeError: If the parsed value is not iterable for tuple conversion.
|
|
449
|
+
"""
|
|
450
|
+
value = self.get(name, section=section, default=None)
|
|
451
|
+
if isinstance(value, tuple):
|
|
452
|
+
return value
|
|
453
|
+
return tuple(ast.literal_eval(value)) if value is not None else default
|
|
454
|
+
|
|
455
|
+
def getdict(
|
|
456
|
+
self, name: str, section: str = "DEFAULT", default: dict[Any, Any] | None = None
|
|
457
|
+
) -> dict[Any, Any] | None:
|
|
458
|
+
"""Get a value and parse it as a dictionary.
|
|
459
|
+
|
|
460
|
+
If the resolved value is already a dict, it is returned as-is.
|
|
461
|
+
|
|
462
|
+
Args:
|
|
463
|
+
name: Configuration key.
|
|
464
|
+
section: Configuration section.
|
|
465
|
+
default: Returned when no value is found.
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
Parsed dictionary value, or default if unresolved.
|
|
469
|
+
|
|
470
|
+
Raises:
|
|
471
|
+
ValueError: If the value contains a malformed literal.
|
|
472
|
+
SyntaxError: If the value is not valid Python literal syntax.
|
|
473
|
+
TypeError: If the parsed value cannot be converted to dict.
|
|
474
|
+
"""
|
|
475
|
+
value = self.get(name, section=section, default=None)
|
|
476
|
+
if isinstance(value, dict):
|
|
477
|
+
return value
|
|
478
|
+
return dict(ast.literal_eval(value)) if value is not None else default
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
__all__ = ["ConfigReader", "ConfigSource"]
|
configreader/py.typed
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ga-configreader
|
|
3
|
+
Version: 0.1.3
|
|
4
|
+
Summary: Read configuration values from INI, DB, environment variables, and dictionaries
|
|
5
|
+
Author-email: Andrea Gemma <andrea.gemma@uniroma3.it>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Documentation, https://github.com/andreagemma/configreader#readme
|
|
8
|
+
Project-URL: Issues, https://github.com/andreagemma/configreader/issues
|
|
9
|
+
Project-URL: Source, https://github.com/andreagemma/configreader
|
|
10
|
+
Keywords: configuration,ini,environment,sqlalchemy,settings
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
19
|
+
Classifier: Topic :: Database :: Front-Ends
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Provides-Extra: db
|
|
26
|
+
Requires-Dist: sqlalchemy>=2.0; extra == "db"
|
|
27
|
+
Provides-Extra: test
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == "test"
|
|
29
|
+
Requires-Dist: pytest-cov>=5.0; extra == "test"
|
|
30
|
+
Provides-Extra: dev
|
|
31
|
+
Requires-Dist: build>=1.2; extra == "dev"
|
|
32
|
+
Requires-Dist: mypy>=1.10; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff>=0.5; extra == "dev"
|
|
36
|
+
Requires-Dist: twine>=5.1; extra == "dev"
|
|
37
|
+
Dynamic: license-file
|
|
38
|
+
|
|
39
|
+
# configreader
|
|
40
|
+
|
|
41
|
+
Python library to read configuration values from multiple sources with configurable precedence.
|
|
42
|
+
|
|
43
|
+
Supported sources:
|
|
44
|
+
- INI file
|
|
45
|
+
- SQL database through SQLAlchemy (optional)
|
|
46
|
+
- environment variables
|
|
47
|
+
- in-memory Python dictionary
|
|
48
|
+
|
|
49
|
+
Values are resolved in order, and the first non-empty match is returned.
|
|
50
|
+
|
|
51
|
+
## Installation
|
|
52
|
+
|
|
53
|
+
### From PyPI
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install ga-configreader
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### From source
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
git clone https://github.com/andreagemma/configreader.git
|
|
63
|
+
cd configreader
|
|
64
|
+
pip install -e .
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Database support (optional)
|
|
68
|
+
|
|
69
|
+
Install SQLAlchemy if you want to use the DB provider:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install sqlalchemy
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Or install the project with DB extras:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
pip install "configreader[db]"
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Quickstart
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from configreader import ConfigReader
|
|
85
|
+
|
|
86
|
+
reader = ConfigReader(
|
|
87
|
+
file="config.ini",
|
|
88
|
+
use_env=True,
|
|
89
|
+
dictionary={"DEFAULT": {"timeout": "30"}},
|
|
90
|
+
providers=["env", "ini", "dict"],
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
host = reader.get("host", default="127.0.0.1")
|
|
94
|
+
port = reader.getint("port", default=8080)
|
|
95
|
+
debug = reader.getboolean("debug", default=False)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Provider Precedence
|
|
99
|
+
|
|
100
|
+
The providers list defines lookup order.
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
providers = ["env", "db", "ini", "dict"]
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Meaning:
|
|
109
|
+
1. check environment first
|
|
110
|
+
2. then check database
|
|
111
|
+
3. then check INI file
|
|
112
|
+
4. then check dictionary
|
|
113
|
+
|
|
114
|
+
## Environment Variables
|
|
115
|
+
|
|
116
|
+
Naming rules:
|
|
117
|
+
- if section="DEFAULT", variable name is NAME
|
|
118
|
+
- for custom sections, variable name is SECTION_NAME
|
|
119
|
+
|
|
120
|
+
Examples:
|
|
121
|
+
- reader.get("host", section="DEFAULT") reads HOST
|
|
122
|
+
- reader.get("host", section="app") reads APP_HOST
|
|
123
|
+
|
|
124
|
+
## Using INI Files
|
|
125
|
+
|
|
126
|
+
Example config.ini:
|
|
127
|
+
|
|
128
|
+
```ini
|
|
129
|
+
[DEFAULT]
|
|
130
|
+
host = localhost
|
|
131
|
+
port = 5432
|
|
132
|
+
debug = true
|
|
133
|
+
items = [1, 2, 3]
|
|
134
|
+
|
|
135
|
+
[app]
|
|
136
|
+
workers = 4
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Code:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
reader = ConfigReader(file="config.ini")
|
|
143
|
+
|
|
144
|
+
host = reader.get("host")
|
|
145
|
+
port = reader.getint("port")
|
|
146
|
+
debug = reader.getboolean("debug")
|
|
147
|
+
items = reader.getlist("items")
|
|
148
|
+
workers = reader.getint("workers", section="app")
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Using a Dictionary
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
reader = ConfigReader(
|
|
155
|
+
dictionary={
|
|
156
|
+
"DEFAULT": {
|
|
157
|
+
"host": "localhost",
|
|
158
|
+
"allowed": "['admin', 'user']",
|
|
159
|
+
},
|
|
160
|
+
"service": {
|
|
161
|
+
"retries": "3",
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
providers=["dict"],
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
allowed = reader.getlist("allowed")
|
|
168
|
+
retries = reader.getint("retries", section="service")
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## Using a Database
|
|
172
|
+
|
|
173
|
+
Constructor:
|
|
174
|
+
|
|
175
|
+
```python
|
|
176
|
+
reader = ConfigReader(
|
|
177
|
+
db_url="sqlite:///settings.db",
|
|
178
|
+
db_query="SELECT value FROM settings WHERE section = :section AND name = :name",
|
|
179
|
+
providers=["db", "env"],
|
|
180
|
+
)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Default query shape:
|
|
184
|
+
|
|
185
|
+
```sql
|
|
186
|
+
SELECT value FROM settings WHERE section = :section AND name = :name
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
DB utility methods:
|
|
190
|
+
|
|
191
|
+
```python
|
|
192
|
+
ok = ConfigReader.check_db_connection("sqlite:///settings.db")
|
|
193
|
+
exists = ConfigReader.check_db_exists("sqlite:///settings.db", table_name="settings")
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
## Main API
|
|
197
|
+
|
|
198
|
+
- `get(name, default=None, section="DEFAULT") -> str | None`
|
|
199
|
+
- `getint(name, default=None, section="DEFAULT") -> int | None`
|
|
200
|
+
- `getboolean(name, default=None, section="DEFAULT") -> bool | None`
|
|
201
|
+
- `getfloat(name, default=None, section="DEFAULT") -> float | None`
|
|
202
|
+
- `getlist(name, default=None, section="DEFAULT") -> list[Any] | None`
|
|
203
|
+
- `getset(name, default=None, section="DEFAULT") -> set[Any] | None`
|
|
204
|
+
- `gettuple(name, default=None, section="DEFAULT") -> tuple[Any, ...] | None`
|
|
205
|
+
- `getdict(name, default=None, section="DEFAULT") -> dict[Any, Any] | None`
|
|
206
|
+
- `items()` iterator over loaded INI entries
|
|
207
|
+
|
|
208
|
+
Full details in [docs/api.md](docs/api.md).
|
|
209
|
+
|
|
210
|
+
## Errors And Type Conversion
|
|
211
|
+
|
|
212
|
+
- `FileNotFoundError` is raised if the provided INI file does not exist.
|
|
213
|
+
- Typed getters (`getint`, `getfloat`, `getlist`, etc.) propagate parsing/conversion errors.
|
|
214
|
+
- If SQLAlchemy is not installed, DB features are unavailable.
|
|
215
|
+
|
|
216
|
+
## Development
|
|
217
|
+
|
|
218
|
+
Install development dependencies:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
pip install -e .[dev]
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Run tests:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
pytest
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
## More Documentation
|
|
231
|
+
|
|
232
|
+
- [docs/overview.md](docs/overview.md)
|
|
233
|
+
- [docs/providers.md](docs/providers.md)
|
|
234
|
+
- [docs/api.md](docs/api.md)
|
|
235
|
+
- [docs/examples.md](docs/examples.md)
|
|
236
|
+
|
|
237
|
+
## License
|
|
238
|
+
|
|
239
|
+
Distributed under the MIT License. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
configreader/__init__.py,sha256=bQHeQEeXGCvs9Pu9DzgkCAkRFEFe3pb1vBWAXPUrSGs,217
|
|
2
|
+
configreader/_version.py,sha256=XEqb2aiIn8fzGE68Mph4ck1FtQqsR_am0wRWvrYPffQ,22
|
|
3
|
+
configreader/configreader.py,sha256=LJC0H5imNXGBgCQqcK9aeXUqbWSo5tLAJar_SbExGWI,16376
|
|
4
|
+
configreader/py.typed,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
5
|
+
ga_configreader-0.1.3.dist-info/licenses/LICENSE,sha256=qu2EXOee5U25eaDf50dc7Rmuysbz9BNo0hhJ2D9vawc,1069
|
|
6
|
+
ga_configreader-0.1.3.dist-info/METADATA,sha256=4wbT64viQ66shO4MIMK_ShnACelOqVwxopQBKPF_C18,5728
|
|
7
|
+
ga_configreader-0.1.3.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
8
|
+
ga_configreader-0.1.3.dist-info/top_level.txt,sha256=OpY58yfn954wkqaetoS2uQ2VJi4Nw6rpv_PYJwPKG7Q,13
|
|
9
|
+
ga_configreader-0.1.3.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Andrea Gemma
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
configreader
|