confarg 0.0.1__tar.gz

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.
Files changed (38) hide show
  1. confarg-0.0.1/PKG-INFO +453 -0
  2. confarg-0.0.1/README.md +439 -0
  3. confarg-0.0.1/pyproject.toml +50 -0
  4. confarg-0.0.1/pyproject.toml.orig +49 -0
  5. confarg-0.0.1/src/confarg/__init__.py +60 -0
  6. confarg-0.0.1/src/confarg/_api.py +405 -0
  7. confarg-0.0.1/src/confarg/_callable.py +599 -0
  8. confarg-0.0.1/src/confarg/_cast.py +63 -0
  9. confarg-0.0.1/src/confarg/_defaults.py +38 -0
  10. confarg-0.0.1/src/confarg/_files.py +534 -0
  11. confarg-0.0.1/src/confarg/_import.py +54 -0
  12. confarg-0.0.1/src/confarg/_merge.py +412 -0
  13. confarg-0.0.1/src/confarg/_parse_cli.py +949 -0
  14. confarg-0.0.1/src/confarg/_parse_env.py +392 -0
  15. confarg-0.0.1/src/confarg/_pipeline.py +118 -0
  16. confarg-0.0.1/src/confarg/_serialize.py +242 -0
  17. confarg-0.0.1/src/confarg/_types.py +739 -0
  18. confarg-0.0.1/src/confarg/cli/__init__.py +15 -0
  19. confarg-0.0.1/src/confarg/cli/_collect.py +464 -0
  20. confarg-0.0.1/src/confarg/cli/argparse/__init__.py +37 -0
  21. confarg-0.0.1/src/confarg/cli/argparse/_build.py +1081 -0
  22. confarg-0.0.1/src/confarg/cli/argparse/_completion.py +362 -0
  23. confarg-0.0.1/src/confarg/cli/argparse/_namespace.py +170 -0
  24. confarg-0.0.1/src/confarg/cli/argparse/_register.py +288 -0
  25. confarg-0.0.1/src/confarg/cli/argparse/_spec.py +147 -0
  26. confarg-0.0.1/src/confarg/cli/click/__init__.py +23 -0
  27. confarg-0.0.1/src/confarg/cli/click/_completion.py +82 -0
  28. confarg-0.0.1/src/confarg/cli/click/_context.py +190 -0
  29. confarg-0.0.1/src/confarg/cli/click/_register.py +189 -0
  30. confarg-0.0.1/src/confarg/cli/cyclopts/__init__.py +21 -0
  31. confarg-0.0.1/src/confarg/cli/cyclopts/_context.py +197 -0
  32. confarg-0.0.1/src/confarg/cli/cyclopts/_register.py +238 -0
  33. confarg-0.0.1/src/confarg/dictexpr/__init__.py +41 -0
  34. confarg-0.0.1/src/confarg/dictexpr/_expressions.py +623 -0
  35. confarg-0.0.1/src/confarg/exceptions.py +111 -0
  36. confarg-0.0.1/src/confarg/typedload/__init__.py +44 -0
  37. confarg-0.0.1/src/confarg/typedload/_coerce.py +315 -0
  38. confarg-0.0.1/src/confarg/typedload/_construct.py +960 -0
confarg-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,453 @@
1
+ Metadata-Version: 2.4
2
+ Name: confarg
3
+ Version: 0.0.1
4
+ Summary: Load and resolve complex configurations from files, environment variables and command line arguments. Keep your favorite CLI library.
5
+ Author: confarg
6
+ Author-email: confarg <280620574+confarg@users.noreply.github.com>
7
+ License-Expression: MPL-2.0
8
+ Requires-Dist: argcomplete>=3.0 ; extra == 'completion'
9
+ Requires-Python: >=3.12
10
+ Project-URL: Documentation, https://confarg.github.io/confarg
11
+ Project-URL: Repository, https://github.com/confarg/confarg
12
+ Provides-Extra: completion
13
+ Description-Content-Type: text/markdown
14
+
15
+ <!-- pytest-markdown-console-file: notest -->
16
+ <p align="center">
17
+ <img src="https://raw.githubusercontent.com/confarg/confarg/master/docs/assets/banner.svg" alt="confarg" />
18
+ </p>
19
+
20
+ # A tool to manage complex configurations
21
+
22
+ > Load and resolve complex configurations from files, environment variables and command line arguments. Keep your data structures and favorite CLI library.
23
+
24
+ `confarg` is a Python library that helps you load configurations in a modular fashion from multiple sources: files, environment variables, and command line arguments.
25
+
26
+ It can handle deeply nested configurations, type unions, derived classes, expressions and variable interpolation, configuration compositions and more, and can integrate with your favorite argument parser library such as `argparse`, `click` or `typer`.
27
+
28
+ `confarg` is not a framework. No decorator, base class or special annotation type required: none are provided. It is just a tool for the deserialization and serialization of complex configurations. Its footprint in your code is typically a few lines of code.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install confarg
34
+ ```
35
+
36
+ `confarg` comes with no dependency, but installing additional libraries such as `pyyaml` unlocks the support of extra configuration file formats.
37
+
38
+ ## TL;DR
39
+
40
+ This library lets you read configurations stored in classes like this
41
+
42
+ ```python
43
+ @dataclass
44
+ class Config:
45
+ value: float
46
+ flag: bool
47
+ subconfig: SubConfig1 | SubConfig2
48
+ ```
49
+
50
+ with this kind of code
51
+
52
+ ```python
53
+ config = confarg.load(Config)
54
+ ```
55
+
56
+ which lets you build a configuration object from files,
57
+
58
+ ```yaml
59
+ value: 1.0
60
+ flag: false
61
+ subconfig:
62
+ foo: 42
63
+ ```
64
+
65
+ but also and simultaneously from environment variables,
66
+
67
+ ```properties
68
+ MYAPP_VALUE=0.0
69
+ ```
70
+
71
+ and command line arguments,
72
+
73
+ ```bash
74
+ myapp --subconfig.foo=33
75
+ ```
76
+
77
+ Still here? Read along, we are just getting started.
78
+
79
+ ## Getting started
80
+
81
+ > All the examples presented in this section (and more) are available in the `examples/` folder.
82
+
83
+ Imagine that you have an app that depends on some parameters that you have collected into a `dataclass` like so:
84
+
85
+ ```python
86
+ @dataclass
87
+ class DBConfig:
88
+ host: str
89
+ port: int
90
+ name: str
91
+ ```
92
+
93
+ In your app, you use `confarg` to instantiate this configuration:
94
+
95
+ ```python
96
+ config = confarg.load(DBConfig)
97
+ ```
98
+
99
+ This allows you to construct a `DBConfig` object by collecting data from three possible sources.
100
+
101
+ 1. From a configuration file. By passing `--config <config_file>` to your app, `confarg` will load the content of the file and fill the `DBConfig` object. For example, a config file could look like so:
102
+
103
+ ```yaml
104
+ # config.yaml
105
+ host: example.com
106
+ port: 1234
107
+ name: mydb
108
+ ```
109
+
110
+ You would then call your application as
111
+
112
+ ```console
113
+ $ myapp.py --config config.yaml
114
+ DBConfig(host='example.com', port=1234, name='mydb')
115
+ ```
116
+
117
+ Configuration files in TOML and JSON formats are also supported.
118
+
119
+ > You can change the default `config` flag to something else using the `config_flag` parameter.
120
+
121
+ 2. From environment variables. You can declare
122
+
123
+ ```properties
124
+ MYAPP_HOST=example.com
125
+ MYAPP_PORT=1234
126
+ MYAPP_NAME=mydb
127
+ ```
128
+
129
+ for the same effect.
130
+
131
+ > Note that the environment variable prefix of your app should actually be passed to `confarg.load` like so:
132
+ >
133
+ > ```python
134
+ > config = confarg.load(DBConfig, env_prefix="MYAPP_")
135
+ > ```
136
+
137
+ 3. From command line arguments.
138
+
139
+ ```console
140
+ $ my_app --host example.com --port 1234 --name mydb
141
+ DBConfig(host='example.com', port=1234, name='mydb')
142
+ ```
143
+
144
+ ### Progressive build-up
145
+
146
+ The examples above presented different sources to feed your configuration. They are not mutually exclusive — in fact, they are intended to be used simultaneously.
147
+
148
+ Note that no one source needs to provide a complete configuration, as long as the configuration resulting from this progressive build-up is complete.
149
+
150
+ For example, taking our previous example, you could have a partial configuration file containing only host information,
151
+
152
+ ```yaml
153
+ # partial_config.yaml
154
+ host: example.com
155
+ port: 1234
156
+ ```
157
+
158
+ and provide the schema name from the command line:
159
+
160
+ ```console
161
+ $ myapp.py --config partial_config.yaml --name mydb
162
+ DBConfig(host='example.com', port=1234, name='mydb')
163
+ ```
164
+
165
+ ### Source precedence
166
+
167
+ Configuration data is read in the following order, later read overwriting existing data:
168
+
169
+ 1. configuration files are read first;
170
+ 2. then environment variables;
171
+ 3. finally, command line arguments.
172
+
173
+ This allows for surgical modifications of configuration files. For example, one could overwrite the schema configuration from our existing `full_config` from the command line like so:
174
+
175
+ ```console
176
+ $ # Overwrite the schema name defined in the config file from the command line
177
+ $ myapp.py --config config.yaml --name otherdb
178
+ DBConfig(host='example.com', port=1234, name='otherdb')
179
+ ```
180
+
181
+ ### Unions
182
+
183
+ Let's say your app needs to support SQLite databases. You now have two different, incompatible DB configurations:
184
+
185
+ ```python
186
+ @dataclass
187
+ class DBServerConfig:
188
+ host: str
189
+ port: int
190
+ name: str
191
+
192
+ @dataclass
193
+ class SQLiteConfig:
194
+ dbpath: str
195
+ ```
196
+
197
+ The DB configuration needs to be either one or the other, which we declare like so:
198
+
199
+ ```python
200
+ type DBConfig = SQLiteConfig | DBServerConfig
201
+ ```
202
+
203
+ `confarg` can handle this new union type and figure out which configuration is desired based on the arguments it got:
204
+
205
+ ```console
206
+ $ # Pass DBServerConfig parameters, and you get a DBServerConfig
207
+ $ myapp.py --host example.com --port 1234 --name mydb
208
+ DBServerConfig(host='example.com', port=1234, name='mydb')
209
+ $ # Pass SQLiteConfig parameters, and you get a SQLiteConfig
210
+ $ myapp.py --dbpath db.sqlite
211
+ SQLiteConfig(dbpath='db.sqlite')
212
+ ```
213
+
214
+ ### Disambiguation tags
215
+
216
+ For simple configurations, the above automatic disambiguation is enough and convenient.
217
+
218
+ In more complex configuration scenarios, this automatic disambiguation may not be not possible. For example, different configurations may share the exact same fields.
219
+
220
+ Even when disambiguation is possible, it may not be obvious to the human eye which object class should be return from the provided parameters.
221
+
222
+ Therefore, by necessity or for the sake of clarity, you can provide the class path of the required configuration by using the `class` tag, like so
223
+
224
+ ```console
225
+ $ # Explicitly ask for a SQLiteConfig
226
+ $ myapp.py --class myapp.SQLiteConfig --dbpath db.sqlite
227
+ SQLiteConfig(dbpath='db.sqlite')
228
+ ```
229
+
230
+ One example where it is necessary to provide the `class` path is to overwrite the configuration with a new class. Without it, command line arguments are added to the configuration, resulting in an invalid input.
231
+
232
+ ```console
233
+ $ # Config file contains a DBServerConfig
234
+ $ myapp.py --config db_server.yaml
235
+ DBServerConfig(host='example.com', port=1234, name='mydb')
236
+ $ # Fails: dbpath is not a DBServerConfig key
237
+ $ myapp.py --config db_server.yaml --dbpath db.sqlite
238
+ ...
239
+ $ # OK: using class signals overwrite existing DB config
240
+ $ myapp.py --config db_server.yaml --class myapp.SQLiteConfig --dbpath db.sqlite
241
+ SQLiteConfig(dbpath='db.sqlite')
242
+ ```
243
+
244
+ ### Inheritance
245
+
246
+ Another way to provide a flexible configuration is to derive akin configuration classes from a common base class.
247
+
248
+ ```python
249
+ @dataclass
250
+ class DBConfig:
251
+ pass
252
+
253
+ @dataclass
254
+ class DBServerConfig(DBConfig):
255
+ host: str
256
+ port: int
257
+ name: str
258
+
259
+ @dataclass
260
+ class SQLiteConfig(DBConfig):
261
+ dbpath: str
262
+ ```
263
+
264
+ This allows configurations to be easily extensible. Contrast with unions, where a class must be explicitly listed to be supported.
265
+
266
+ The downside is that the concrete class must be tagged, as `confarg` cannot discover classes derived from a given class.
267
+
268
+ ```console
269
+ $ # Fails: derived class not specified
270
+ $ uv run myapp.py --dbpath db.sqlite
271
+ ...
272
+ $ # OK: explicit class path provided
273
+ $ uv run myapp.py --dbpath db.sqlite --class myapp.SQLiteConfig
274
+ SQLiteConfig(dbpath='db.sqlite')
275
+ ```
276
+
277
+ ### Configuration hierarchies
278
+
279
+ The configurations discussed so far has been rather simple, composed of values grouped together in a `dataclass`. However, it needs not be. Configurations are generally deeply nested hierarchies, which `confarg` supports.
280
+
281
+ Let's say you want to add a log level to your application. You place it at the root level of a new `Config` object, along with the DB configuration, that is now one level down under the `db` key.
282
+
283
+ ```python
284
+ @dataclass
285
+ class Config:
286
+ db: DBConfig
287
+ log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
288
+ ```
289
+
290
+ You now parse your new top-level `Config` instead of `DBConfig`.
291
+
292
+ ```python
293
+ config = confarg.load(Config)
294
+ ```
295
+
296
+ Our DB configuration, which used to be the root configuration, is now located under the `db` key. This has the following impact.
297
+
298
+ For command line arguments, we follow the common convention of using dot-separated paths to address nested fields. Previous command line arguments for `DBConfig` are now prefixed by `db.`, like so:
299
+
300
+ ```console
301
+ $ myapp.py --db.class myapp.SQLiteConfig --db.dbpath db.sqlite
302
+ Config(db=SQLiteConfig(dbpath='db.sqlite'), log_level='INFO')
303
+ ```
304
+
305
+ The configuration file is also modified accordingly,
306
+
307
+ ```yaml
308
+ # config.yaml
309
+ db:
310
+ class: myapp.DBServerConfig
311
+ host: example.com
312
+ name: mydb
313
+ port: 1234
314
+ ```
315
+
316
+ and is used just like before:
317
+
318
+ ```console
319
+ $ myapp.py --config config.yaml
320
+ Config(db=DBServerConfig(host='example.com', port=1234, name='mydb'),
321
+ log_level='DEBUG')
322
+ ```
323
+
324
+ ### Leaf data type and type coercion
325
+
326
+ You may have noticed that the previous section introduced a `log_level` parameter that has two interesting features: first, it is not of a simple type (`str`, `int`, `float`, `bool` or `None`); second, it comes with a default value.
327
+
328
+ Default values are honored, and you may have noticed that we did not provide any value to `log_level`. You can of course override a default value.
329
+
330
+ As for leaf node data type, `confarg` coerces `Enum` and `Path` types as special exceptions to simple types. Other types are treated as classes and must follow the same rules.
331
+
332
+ ### Expressions and variable interpolation
333
+
334
+ Your application is becoming more complex by the day, and is now requiring a resources configuration.
335
+
336
+ ```python
337
+ @dataclass
338
+ class Resources:
339
+ cpu_count: int
340
+ memory_gb: int
341
+ max_heap_size_mb: int
342
+ ```
343
+
344
+ It is added to the global configuration under the `resources` key:
345
+
346
+ ```python
347
+ @dataclass
348
+ class Config:
349
+ db: DBConfig
350
+ resources: Resources
351
+ log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
352
+ ```
353
+
354
+ Your configuration file has become,
355
+
356
+ ```yaml
357
+ # config.yaml
358
+ db:
359
+ class: myapp.DBServerConfig
360
+ host: example.com
361
+ name: mydb
362
+ port: 1234
363
+
364
+ resources:
365
+ cpu_count: 4
366
+ memory_gb: 16
367
+ max_heap_size_mb: 131072
368
+ ```
369
+
370
+ This works fine. However, you want to better express the fact that `max_heap_size_mb` is chosen to be 80% of the host memory by default. To achieve this, you can write expressions relying on variable interpolation using the `${...}` syntax, like so:
371
+
372
+ ```yaml
373
+ # expression_config.yaml
374
+ db:
375
+ class: myapp.DBServerConfig
376
+ host: example.com
377
+ name: mydb
378
+ port: 1234
379
+
380
+ resources:
381
+ cpu_count: 4
382
+ memory_gb: 16
383
+ max_heap_size_mb: ${int(resources.memory_gb * 1024 * 0.8)}
384
+ ```
385
+
386
+ ```console
387
+ $ myapp.py --config expression_config.yaml
388
+ Config(db=SQLiteConfig(dbpath='db.sqlite'),
389
+ resources=Resources(cpu_count=4, memory_gb=16, max_heap_size_mb=13107),
390
+ log_level='INFO')
391
+ ```
392
+
393
+ Note that variable interpolation occurs after all configuration data is read. This means here that you can override `memory_gb` from the command line, and `max_heap_size_mb` will be adjusted accordingly, even though the expression is defined in the configuration file.
394
+
395
+ ```console
396
+ $ # Max heap is recomputed according to the expression in the config file
397
+ $ myapp.py --config expression_config.yaml --resources.memory_gb 8
398
+ Config(db=SQLiteConfig(dbpath='db.sqlite'),
399
+ resources=Resources(cpu_count=4, memory_gb=8, max_heap_size_mb=6553),
400
+ log_level='INFO')
401
+ ```
402
+
403
+ ### Building large configurations from parts
404
+
405
+ Large configurations are often made up of independent components, and as such, you may want to split them accordingly. It is easier to navigate, but it also makes it possible to reuse configuration parts and to build multiple complex configurations from the same set of atomic configuration components.
406
+
407
+ Some configuration components may even be generated automatically, in which case being able to isolate those parts from the rest is a must.
408
+
409
+ `confarg` lets you do this in different ways.
410
+
411
+ From the command line, the `--config` flag can be suffixed with a key path to load configurations there. For example,
412
+
413
+ ```console
414
+ # Load a config file specific to the `db` key
415
+ $ myapp.py --config.db db_config.yaml
416
+ Config(db=DBServerConfig(host='example.com', port=1234, name='mydb'), log_level='INFO')
417
+ ```
418
+
419
+ A similar pattern applies to environment variables:
420
+
421
+ ```console
422
+ $ MYAPP_CONFIG_DB=db_config.py myapp.py
423
+ Config(db=DBServerConfig(host='example.com', port=1234, name='mydb'), log_level='INFO')
424
+ ```
425
+
426
+ > Note that `db_config.yaml` does *not* contain the `db` key. It does not need to know the path it is loaded to.
427
+
428
+ In config files, you can load a configuration by specifying the special `__include__` key, followed by the path to the sub-configuration to load, like so:
429
+
430
+ ```yaml
431
+ # set everything under the `db` key from another file
432
+ db:
433
+ __include__: ./db_config.yaml
434
+ ```
435
+
436
+ The `__include__` keyword can also be used at the top-level, to create a new config that amends an existing config.
437
+
438
+ ```yaml
439
+ # start from this base configuration
440
+ __include__: base_config.yaml
441
+
442
+ # set or overwrite everything under the `db` key
443
+ db:
444
+ __include__: ./db_config.yaml
445
+ ```
446
+
447
+ ## Next steps
448
+
449
+ We have more than scratched the surface, and you should have enough knowledge to cover most of your needs.
450
+
451
+ Again, all of the examples above and more are in the `examples/` folder, which is a great way to discover and experiment with the library features.
452
+
453
+ A documentation is also currently being written at https://confarg.github.io/confarg/.