config2py 0.1.41__tar.gz → 0.1.43__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 (23) hide show
  1. {config2py-0.1.41/config2py.egg-info → config2py-0.1.43}/PKG-INFO +85 -9
  2. config2py-0.1.41/PKG-INFO → config2py-0.1.43/README.md +79 -17
  3. {config2py-0.1.41 → config2py-0.1.43}/config2py/__init__.py +3 -0
  4. {config2py-0.1.41 → config2py-0.1.43}/config2py/base.py +6 -8
  5. {config2py-0.1.41 → config2py-0.1.43}/config2py/s_configparser.py +6 -5
  6. config2py-0.1.43/config2py/sync_store.py +380 -0
  7. {config2py-0.1.41 → config2py-0.1.43}/config2py/tests/__init__.py +2 -0
  8. config2py-0.1.43/config2py/tests/test_sync_store.py +316 -0
  9. {config2py-0.1.41 → config2py-0.1.43}/config2py/tests/utils_for_testing.py +2 -0
  10. {config2py-0.1.41 → config2py-0.1.43}/config2py/tools.py +1 -1
  11. {config2py-0.1.41 → config2py-0.1.43}/config2py/util.py +45 -23
  12. config2py-0.1.41/README.md → config2py-0.1.43/config2py.egg-info/PKG-INFO +93 -7
  13. {config2py-0.1.41 → config2py-0.1.43}/config2py.egg-info/SOURCES.txt +2 -0
  14. {config2py-0.1.41 → config2py-0.1.43}/setup.cfg +1 -1
  15. {config2py-0.1.41 → config2py-0.1.43}/LICENSE +0 -0
  16. {config2py-0.1.41 → config2py-0.1.43}/config2py/errors.py +0 -0
  17. {config2py-0.1.41 → config2py-0.1.43}/config2py/scrap/__init__.py +0 -0
  18. {config2py-0.1.41 → config2py-0.1.43}/config2py/tests/test_tools.py +0 -0
  19. {config2py-0.1.41 → config2py-0.1.43}/config2py.egg-info/dependency_links.txt +0 -0
  20. {config2py-0.1.41 → config2py-0.1.43}/config2py.egg-info/not-zip-safe +0 -0
  21. {config2py-0.1.41 → config2py-0.1.43}/config2py.egg-info/requires.txt +0 -0
  22. {config2py-0.1.41 → config2py-0.1.43}/config2py.egg-info/top_level.txt +0 -0
  23. {config2py-0.1.41 → config2py-0.1.43}/setup.py +0 -0
@@ -1,12 +1,16 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: config2py
3
- Version: 0.1.41
3
+ Version: 0.1.43
4
4
  Summary: Simplified reading and writing configurations from various sources and formats
5
5
  Home-page: https://github.com/i2mint/config2py
6
6
  License: apache-2.0
7
7
  Platform: any
8
8
  Description-Content-Type: text/markdown
9
9
  License-File: LICENSE
10
+ Requires-Dist: dol
11
+ Requires-Dist: i2
12
+ Requires-Dist: importlib_resources; python_version < "3.9"
13
+ Dynamic: license-file
10
14
 
11
15
  # config2py
12
16
 
@@ -107,7 +111,7 @@ In fact, `simple_config_getter` is a function to make configuration getters that
107
111
 
108
112
  <img width="341" alt="image" src="https://github.com/i2mint/config2py/assets/1906276/09f287a8-05f9-4590-8664-10feda9ad617">
109
113
 
110
- But where you can control what the central store (by default "Local App Data Files" store) is, and whether to first search in environment variables or not, and whether to ask the user for the value, if not found before, or not.
114
+ But where you can control what the central store (by default a local configuration files store) is, and whether to first search in environment variables or not, and whether to ask the user for the value, if not found before, or not.
111
115
 
112
116
  ```python
113
117
  from config2py import simple_config_getter, get_configs_local_store
@@ -127,7 +131,7 @@ print(*str(Sig(simple_config_getter)).split(','), sep='\n')
127
131
  `ask_user_if_key_not_found` specifies whether to ask the user if a configuration key is not found. The default is `None`, which will result in checking if you're running in an interactive environment or not.
128
132
  When you use `config2py` in production though, you should definitely specify `ask_user_if_key_not_found=False` to make that choice explicit.
129
133
 
130
- The `configs_src` default is automatically set to be the `config2py/configs` folder of your systems's "App Data" folder (also configurable via a `CONFIG2PY_APP_DATA_FOLDER` environment variable).
134
+ The `configs_src` default is automatically set to be the `config2py/configs` folder of your system's config directory (following XDG standards on Unix/Linux/macOS). You can override this with environment variables like `CONFIG2PY_CONFIG_DIR`, `CONFIG2PY_DATA_DIR`, etc., or the standard XDG variables.
131
135
 
132
136
  Your central store will be `config_store_factory(configs_src)`, and since you can also specify `config_store_factory`, you have total control over the store.
133
137
 
@@ -207,12 +211,88 @@ It will return the value that the user entered last time, without prompting the
207
211
  user again.
208
212
 
209
213
 
214
+ ## SyncStore: Auto-Syncing Key-Value Stores
215
+
216
+ ### Overview
217
+
218
+ `SyncStore` provides MutableMapping interfaces that automatically persist changes to backing storage. Changes sync immediately by default, or can be deferred using a context manager for efficient batch operations.
219
+
220
+ ### Basic Usage
221
+
222
+ ```python
223
+ from config2py.sync_store import FileStore, JsonStore
224
+
225
+ # Auto-detected from .json extension
226
+ config = FileStore('config.json')
227
+ config['api_key'] = 'secret' # Syncs immediately
228
+
229
+ # Batch operations (deferred sync)
230
+ with config:
231
+ config['a'] = 1
232
+ config['b'] = 2
233
+ config['c'] = 3
234
+ # Syncs once on exit
235
+ ```
236
+
237
+ ### Nested Sections
238
+
239
+ ```python
240
+ # Work with specific section via key_path
241
+ db_config = FileStore('config.json', key_path='database')
242
+ db_config['host'] = 'localhost' # Only affects database section
243
+
244
+ # Dotted notation for deep nesting
245
+ items = FileStore('config.json', key_path='app.settings.items')
246
+ items['item1'] = 'value'
247
+ ```
248
+
249
+ ### Supported Formats
250
+
251
+ Auto-detected by extension:
252
+ - `.json` - JSON (stdlib)
253
+ - `.ini`, `.cfg` - INI files (stdlib)
254
+ - `.yaml`, `.yml` - YAML (if PyYAML installed)
255
+ - `.toml` - TOML (if tomli/tomllib installed)
256
+
257
+ Register custom formats:
258
+ ```python
259
+ from sync_store import register_extension
260
+
261
+ register_extension('.custom', my_loader, my_dumper)
262
+ store = FileStore('data.custom')
263
+ ```
264
+
265
+ ### Custom Backing Storage
266
+
267
+ ```python
268
+ from config2py.sync_store import SyncStore
269
+
270
+ # Any backing storage via loader/dumper
271
+ def my_loader():
272
+ return fetch_from_database()
273
+
274
+ def my_dumper(data):
275
+ save_to_database(data)
276
+
277
+ store = SyncStore(my_loader, my_dumper)
278
+ store['key'] = 'value' # Calls my_dumper
279
+ ```
280
+
281
+ ### Key Classes
282
+
283
+ - **`SyncStore`** - Base class with loader/dumper functions
284
+ - **`FileStore`** - File-based with extension detection and key_path
285
+ - **`JsonStore`** - Explicit JSON with sensible defaults
286
+
287
+
210
288
  # A few notable tools you can import from config2py
211
289
 
212
290
  * `get_config`: Get a config value from a list of sources. See more below.
213
291
  * `user_gettable`: Create a ``GettableContainer`` that asks the user for a value, optionally saving it.
214
292
  * `ask_user_for_input`: Ask the user for input, optionally masking, validating and transforming the input.
215
- * `get_app_config_folder`: Returns the full path of a directory suitable for storing application-specific data for a given app name.
293
+ * `get_app_folder`: Returns the full path of a directory suitable for storing application-specific data for a given app name and folder kind (config, data, cache, state, runtime).
294
+ * `get_app_config_folder`: Specialized version of `get_app_folder` for configuration files.
295
+ * `get_app_data_folder`: Specialized version of `get_app_folder` for application data.
216
296
  * `get_configs_local_store`: Get a local store (mapping interface of local files) of configs for a given app or package name
217
297
  * `configs`: A default store instance for configs, defaulting to a local store under a default configuration local directory.
218
298
 
@@ -335,7 +415,3 @@ s['SOME_KEY']
335
415
 
336
416
  More on that another day...
337
417
 
338
-
339
- ```python
340
-
341
- ```
@@ -1,13 +1,3 @@
1
- Metadata-Version: 2.1
2
- Name: config2py
3
- Version: 0.1.41
4
- Summary: Simplified reading and writing configurations from various sources and formats
5
- Home-page: https://github.com/i2mint/config2py
6
- License: apache-2.0
7
- Platform: any
8
- Description-Content-Type: text/markdown
9
- License-File: LICENSE
10
-
11
1
  # config2py
12
2
 
13
3
  Simplified reading and writing configurations from various sources and formats.
@@ -107,7 +97,7 @@ In fact, `simple_config_getter` is a function to make configuration getters that
107
97
 
108
98
  <img width="341" alt="image" src="https://github.com/i2mint/config2py/assets/1906276/09f287a8-05f9-4590-8664-10feda9ad617">
109
99
 
110
- But where you can control what the central store (by default "Local App Data Files" store) is, and whether to first search in environment variables or not, and whether to ask the user for the value, if not found before, or not.
100
+ But where you can control what the central store (by default a local configuration files store) is, and whether to first search in environment variables or not, and whether to ask the user for the value, if not found before, or not.
111
101
 
112
102
  ```python
113
103
  from config2py import simple_config_getter, get_configs_local_store
@@ -127,7 +117,7 @@ print(*str(Sig(simple_config_getter)).split(','), sep='\n')
127
117
  `ask_user_if_key_not_found` specifies whether to ask the user if a configuration key is not found. The default is `None`, which will result in checking if you're running in an interactive environment or not.
128
118
  When you use `config2py` in production though, you should definitely specify `ask_user_if_key_not_found=False` to make that choice explicit.
129
119
 
130
- The `configs_src` default is automatically set to be the `config2py/configs` folder of your systems's "App Data" folder (also configurable via a `CONFIG2PY_APP_DATA_FOLDER` environment variable).
120
+ The `configs_src` default is automatically set to be the `config2py/configs` folder of your system's config directory (following XDG standards on Unix/Linux/macOS). You can override this with environment variables like `CONFIG2PY_CONFIG_DIR`, `CONFIG2PY_DATA_DIR`, etc., or the standard XDG variables.
131
121
 
132
122
  Your central store will be `config_store_factory(configs_src)`, and since you can also specify `config_store_factory`, you have total control over the store.
133
123
 
@@ -207,12 +197,88 @@ It will return the value that the user entered last time, without prompting the
207
197
  user again.
208
198
 
209
199
 
200
+ ## SyncStore: Auto-Syncing Key-Value Stores
201
+
202
+ ### Overview
203
+
204
+ `SyncStore` provides MutableMapping interfaces that automatically persist changes to backing storage. Changes sync immediately by default, or can be deferred using a context manager for efficient batch operations.
205
+
206
+ ### Basic Usage
207
+
208
+ ```python
209
+ from config2py.sync_store import FileStore, JsonStore
210
+
211
+ # Auto-detected from .json extension
212
+ config = FileStore('config.json')
213
+ config['api_key'] = 'secret' # Syncs immediately
214
+
215
+ # Batch operations (deferred sync)
216
+ with config:
217
+ config['a'] = 1
218
+ config['b'] = 2
219
+ config['c'] = 3
220
+ # Syncs once on exit
221
+ ```
222
+
223
+ ### Nested Sections
224
+
225
+ ```python
226
+ # Work with specific section via key_path
227
+ db_config = FileStore('config.json', key_path='database')
228
+ db_config['host'] = 'localhost' # Only affects database section
229
+
230
+ # Dotted notation for deep nesting
231
+ items = FileStore('config.json', key_path='app.settings.items')
232
+ items['item1'] = 'value'
233
+ ```
234
+
235
+ ### Supported Formats
236
+
237
+ Auto-detected by extension:
238
+ - `.json` - JSON (stdlib)
239
+ - `.ini`, `.cfg` - INI files (stdlib)
240
+ - `.yaml`, `.yml` - YAML (if PyYAML installed)
241
+ - `.toml` - TOML (if tomli/tomllib installed)
242
+
243
+ Register custom formats:
244
+ ```python
245
+ from sync_store import register_extension
246
+
247
+ register_extension('.custom', my_loader, my_dumper)
248
+ store = FileStore('data.custom')
249
+ ```
250
+
251
+ ### Custom Backing Storage
252
+
253
+ ```python
254
+ from config2py.sync_store import SyncStore
255
+
256
+ # Any backing storage via loader/dumper
257
+ def my_loader():
258
+ return fetch_from_database()
259
+
260
+ def my_dumper(data):
261
+ save_to_database(data)
262
+
263
+ store = SyncStore(my_loader, my_dumper)
264
+ store['key'] = 'value' # Calls my_dumper
265
+ ```
266
+
267
+ ### Key Classes
268
+
269
+ - **`SyncStore`** - Base class with loader/dumper functions
270
+ - **`FileStore`** - File-based with extension detection and key_path
271
+ - **`JsonStore`** - Explicit JSON with sensible defaults
272
+
273
+
210
274
  # A few notable tools you can import from config2py
211
275
 
212
276
  * `get_config`: Get a config value from a list of sources. See more below.
213
277
  * `user_gettable`: Create a ``GettableContainer`` that asks the user for a value, optionally saving it.
214
278
  * `ask_user_for_input`: Ask the user for input, optionally masking, validating and transforming the input.
215
- * `get_app_config_folder`: Returns the full path of a directory suitable for storing application-specific data for a given app name.
279
+ * `get_app_folder`: Returns the full path of a directory suitable for storing application-specific data for a given app name and folder kind (config, data, cache, state, runtime).
280
+ * `get_app_config_folder`: Specialized version of `get_app_folder` for configuration files.
281
+ * `get_app_data_folder`: Specialized version of `get_app_folder` for application data.
216
282
  * `get_configs_local_store`: Get a local store (mapping interface of local files) of configs for a given app or package name
217
283
  * `configs`: A default store instance for configs, defaulting to a local store under a default configuration local directory.
218
284
 
@@ -335,7 +401,3 @@ s['SOME_KEY']
335
401
 
336
402
  More on that another day...
337
403
 
338
-
339
- ```python
340
-
341
- ```
@@ -16,8 +16,11 @@ from config2py.util import (
16
16
  envvar, # os.environ, but with dict display override to hide secrets
17
17
  ask_user_for_input,
18
18
  get_app_config_folder,
19
+ get_app_data_folder,
20
+ get_app_folder,
19
21
  get_configs_folder_for_app,
20
22
  is_repl,
21
23
  parse_assignments_from_py_source,
22
24
  process_path,
23
25
  )
26
+ from config2py.sync_store import SyncStore, FileStore, JsonStore, register_extension
@@ -4,26 +4,24 @@ Base for getting configs from various sources and formats
4
4
 
5
5
  from collections import ChainMap
6
6
  from typing import (
7
- Callable,
8
7
  Type,
9
8
  Tuple,
10
9
  KT,
11
10
  VT,
12
11
  Any,
13
- Iterable,
14
12
  Protocol,
15
13
  Union,
16
14
  runtime_checkable,
17
15
  Optional,
18
- MutableMapping,
19
16
  )
17
+ from collections.abc import Callable, Iterable, MutableMapping
20
18
  from dataclasses import dataclass
21
19
  from functools import lru_cache, partial
22
20
 
23
21
  from config2py.util import always_true, ask_user_for_input, no_default, not_found
24
22
  from config2py.errors import ConfigNotFound
25
23
 
26
- Exceptions = Tuple[Type[Exception], ...]
24
+ Exceptions = tuple[type[Exception], ...]
27
25
 
28
26
 
29
27
  @runtime_checkable
@@ -104,8 +102,8 @@ def get_config(
104
102
  sources: Sources = None,
105
103
  *,
106
104
  default: VT = no_default,
107
- egress: Optional[GetConfigEgress] = None,
108
- val_is_valid: Optional[Callable[[VT], bool]] = always_true,
105
+ egress: GetConfigEgress | None = None,
106
+ val_is_valid: Callable[[VT], bool] | None = always_true,
109
107
  config_not_found_exceptions: Exceptions = (Exception,),
110
108
  ):
111
109
  """Get a config value from a list of sources
@@ -374,7 +372,7 @@ def ask_user_for_key(
374
372
  save_to: SaveTo = None,
375
373
  save_condition=is_not_empty,
376
374
  user_asker=ask_user_for_input,
377
- egress: Optional[Callable] = None,
375
+ egress: Callable | None = None,
378
376
  ):
379
377
  if key is None:
380
378
  return partial(
@@ -399,7 +397,7 @@ def user_gettable(
399
397
  save_to: SaveTo = None,
400
398
  *,
401
399
  prompt_template="Enter a value for {}: ",
402
- egress: Optional[Callable] = None,
400
+ egress: Callable | None = None,
403
401
  user_asker=ask_user_for_input,
404
402
  val_is_valid: Callable[[VT], bool] = is_not_empty,
405
403
  config_not_found_exceptions: Exceptions = (Exception,),
@@ -299,11 +299,11 @@ class ConfigStore(ConfigParserStore):
299
299
 
300
300
  @persist_after_operation
301
301
  def __setitem__(self, k, v):
302
- super(ConfigStore, self).__setitem__(k, v)
302
+ super().__setitem__(k, v)
303
303
 
304
304
  @persist_after_operation
305
305
  def __delitem__(self, k):
306
- super(ConfigStore, self).__delitem__(k)
306
+ super().__delitem__(k)
307
307
 
308
308
  # __setitem__ = super_and_persist(ConfigParser, '__setitem__')
309
309
  # __delitem__ = super_and_persist(ConfigParser, '__delitem__')
@@ -388,13 +388,14 @@ class ConfigReader(ConfigStore):
388
388
  # return super()._obj_of_data(data)
389
389
 
390
390
 
391
- from typing import Mapping, Iterable, Generator, Union
391
+ from typing import Union
392
+ from collections.abc import Mapping, Iterable, Generator
392
393
  import re
393
394
 
394
395
 
395
396
  # TODO: postprocess_ini_section_items and preprocess_ini_section_items: Add comma separated possibility?
396
397
  # TODO: Find out if configparse has an option to do this processing alreadys
397
- def postprocess_ini_section_items(items: Union[Mapping, Iterable]) -> Generator:
398
+ def postprocess_ini_section_items(items: Mapping | Iterable) -> Generator:
398
399
  r"""Transform newline-separated string values into actual list of strings (assuming that intent)
399
400
 
400
401
  >>> section_from_ini = {
@@ -417,7 +418,7 @@ def postprocess_ini_section_items(items: Union[Mapping, Iterable]) -> Generator:
417
418
 
418
419
 
419
420
  # TODO: Find out if configparse has an option to do this processing alreadys
420
- def preprocess_ini_section_items(items: Union[Mapping, Iterable]) -> Generator:
421
+ def preprocess_ini_section_items(items: Mapping | Iterable) -> Generator:
421
422
  """Transform list values into newline-separated strings, in view of writing the value to a ini formatted section
422
423
  >>> section = {
423
424
  ... 'name': 'aspyre',