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.
@@ -0,0 +1,8 @@
1
+ """configreader package
2
+ Simple library functions for the project.
3
+ """
4
+
5
+ from ._version import __version__
6
+ from .configreader import ConfigReader, ConfigSource
7
+
8
+ __all__ = ["__version__", "ConfigReader", "ConfigSource"]
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -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