python-simpleconf 0.7.2__tar.gz → 0.8.0__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 (20) hide show
  1. {python_simpleconf-0.7.2 → python_simpleconf-0.8.0}/PKG-INFO +30 -13
  2. {python_simpleconf-0.7.2 → python_simpleconf-0.8.0}/README.md +23 -9
  3. {python_simpleconf-0.7.2 → python_simpleconf-0.8.0}/pyproject.toml +9 -5
  4. {python_simpleconf-0.7.2 → python_simpleconf-0.8.0}/simpleconf/__init__.py +1 -1
  5. {python_simpleconf-0.7.2 → python_simpleconf-0.8.0}/simpleconf/config.py +203 -4
  6. python_simpleconf-0.8.0/simpleconf/loaders/__init__.py +134 -0
  7. {python_simpleconf-0.7.2 → python_simpleconf-0.8.0}/simpleconf/loaders/dict.py +4 -0
  8. {python_simpleconf-0.7.2 → python_simpleconf-0.8.0}/simpleconf/loaders/env.py +23 -11
  9. {python_simpleconf-0.7.2 → python_simpleconf-0.8.0}/simpleconf/loaders/ini.py +31 -16
  10. python_simpleconf-0.8.0/simpleconf/loaders/json.py +47 -0
  11. {python_simpleconf-0.7.2 → python_simpleconf-0.8.0}/simpleconf/loaders/osenv.py +15 -11
  12. {python_simpleconf-0.7.2 → python_simpleconf-0.8.0}/simpleconf/loaders/toml.py +22 -1
  13. {python_simpleconf-0.7.2 → python_simpleconf-0.8.0}/simpleconf/loaders/yaml.py +20 -1
  14. python_simpleconf-0.7.2/simpleconf/loaders/__init__.py +0 -63
  15. python_simpleconf-0.7.2/simpleconf/loaders/json.py +0 -28
  16. {python_simpleconf-0.7.2 → python_simpleconf-0.8.0}/LICENSE +0 -0
  17. {python_simpleconf-0.7.2 → python_simpleconf-0.8.0}/simpleconf/caster.py +0 -0
  18. {python_simpleconf-0.7.2 → python_simpleconf-0.8.0}/simpleconf/exceptions.py +0 -0
  19. {python_simpleconf-0.7.2 → python_simpleconf-0.8.0}/simpleconf/py.typed +0 -0
  20. {python_simpleconf-0.7.2 → python_simpleconf-0.8.0}/simpleconf/utils.py +0 -0
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: python-simpleconf
3
- Version: 0.7.2
3
+ Version: 0.8.0
4
4
  Summary: Simple configuration management with python.
5
5
  License: MIT
6
+ License-File: LICENSE
6
7
  Author: pwwang
7
8
  Author-email: pwwang@pwwang.com
8
9
  Requires-Python: >=3.9,<4.0
@@ -13,19 +14,21 @@ Classifier: Programming Language :: Python :: 3.10
13
14
  Classifier: Programming Language :: Python :: 3.11
14
15
  Classifier: Programming Language :: Python :: 3.12
15
16
  Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
16
18
  Provides-Extra: all
17
- Provides-Extra: cloud
19
+ Provides-Extra: async
18
20
  Provides-Extra: env
19
21
  Provides-Extra: ini
20
22
  Provides-Extra: toml
21
23
  Provides-Extra: yaml
24
+ Requires-Dist: aiofiles (>=23.0.0) ; extra == "async"
22
25
  Requires-Dist: diot (>=0.3.2,<0.4.0)
23
26
  Requires-Dist: iniconfig (>=2.0,<3.0) ; extra == "ini" or extra == "all"
27
+ Requires-Dist: panpath (>=0.4.8,<0.5.0)
24
28
  Requires-Dist: python-dotenv (>=1.1,<2.0) ; extra == "env" or extra == "all"
25
29
  Requires-Dist: pyyaml (>=6,<7) ; extra == "yaml" or extra == "all"
26
30
  Requires-Dist: rtoml (>=0.12,<0.13) ; (sys_platform == "linux") and (extra == "toml" or extra == "all")
27
31
  Requires-Dist: tomli (>=2.0,<3.0) ; (sys_platform != "linux") and (extra == "toml" or extra == "all")
28
- Requires-Dist: yunpath (>=0.0.4,<0.0.5) ; extra == "cloud"
29
32
  Project-URL: Homepage, https://github.com/pwwang/simpleconf
30
33
  Project-URL: Repository, https://github.com/pwwang/simpleconf
31
34
  Description-Content-Type: text/markdown
@@ -60,24 +63,34 @@ pip install python-simpleconf[all]
60
63
  - Type casting
61
64
  - Profile support
62
65
  - Simple APIs
66
+ - Async loading support
63
67
 
64
68
  ## Usage
65
69
 
66
70
  ### Loading configurations
67
71
 
68
72
  ```python
73
+ import asyncio
69
74
  from simpleconf import Config
70
75
 
71
- # Load a single file
72
- conf = Config.load('~/xxx.ini')
73
- # load multiple files, later files override previous ones
74
- conf = Config.load(
75
- '~/xxx.ini', '~/xxx.env', '~/xxx.yaml', '~/xxx.toml',
76
- '~/xxx.json', 'simpleconf.osenv', {'a': 3}
77
- )
78
76
 
79
- # Load a single file with a different loader
80
- conf = Config.load('~/xxx.ini', loader="toml")
77
+ async def main():
78
+ # Load a single file
79
+ conf = Config.load('~/xxx.ini')
80
+ # load multiple files, later files override previous ones
81
+ conf = Config.load(
82
+ '~/xxx.ini', '~/xxx.env', '~/xxx.yaml', '~/xxx.toml',
83
+ '~/xxx.json', 'simpleconf.osenv', {'a': 3}
84
+ )
85
+
86
+ # Load a single file with a different loader
87
+ conf = Config.load('~/xxx.ini', loader="toml")
88
+
89
+ # Async loading
90
+ conf = await Config.a_load('~/xxx.ini')
91
+
92
+ if __name__ == "__main__":
93
+ asyncio.run(main())
81
94
  ```
82
95
 
83
96
  ### Accessing configuration values
@@ -115,6 +128,10 @@ from simpleconf import ProfileConfig
115
128
 
116
129
  conf = ProfileConfig.load({'default': {'a': 1})
117
130
  # conf.a == 1
131
+
132
+ # Asynchronous loading
133
+ # conf = await ProfileConfig.a_load({'default': {'a': 1})
134
+ # conf.a == 1
118
135
  ```
119
136
 
120
137
  ##### Loading a `.env` file
@@ -28,24 +28,34 @@ pip install python-simpleconf[all]
28
28
  - Type casting
29
29
  - Profile support
30
30
  - Simple APIs
31
+ - Async loading support
31
32
 
32
33
  ## Usage
33
34
 
34
35
  ### Loading configurations
35
36
 
36
37
  ```python
38
+ import asyncio
37
39
  from simpleconf import Config
38
40
 
39
- # Load a single file
40
- conf = Config.load('~/xxx.ini')
41
- # load multiple files, later files override previous ones
42
- conf = Config.load(
43
- '~/xxx.ini', '~/xxx.env', '~/xxx.yaml', '~/xxx.toml',
44
- '~/xxx.json', 'simpleconf.osenv', {'a': 3}
45
- )
46
41
 
47
- # Load a single file with a different loader
48
- conf = Config.load('~/xxx.ini', loader="toml")
42
+ async def main():
43
+ # Load a single file
44
+ conf = Config.load('~/xxx.ini')
45
+ # load multiple files, later files override previous ones
46
+ conf = Config.load(
47
+ '~/xxx.ini', '~/xxx.env', '~/xxx.yaml', '~/xxx.toml',
48
+ '~/xxx.json', 'simpleconf.osenv', {'a': 3}
49
+ )
50
+
51
+ # Load a single file with a different loader
52
+ conf = Config.load('~/xxx.ini', loader="toml")
53
+
54
+ # Async loading
55
+ conf = await Config.a_load('~/xxx.ini')
56
+
57
+ if __name__ == "__main__":
58
+ asyncio.run(main())
49
59
  ```
50
60
 
51
61
  ### Accessing configuration values
@@ -83,6 +93,10 @@ from simpleconf import ProfileConfig
83
93
 
84
94
  conf = ProfileConfig.load({'default': {'a': 1})
85
95
  # conf.a == 1
96
+
97
+ # Asynchronous loading
98
+ # conf = await ProfileConfig.a_load({'default': {'a': 1})
99
+ # conf.a == 1
86
100
  ```
87
101
 
88
102
  ##### Loading a `.env` file
@@ -4,7 +4,7 @@ build-backend = "poetry.masonry.api"
4
4
 
5
5
  [tool.poetry]
6
6
  name = "python-simpleconf"
7
- version = "0.7.2"
7
+ version = "0.8.0"
8
8
  description = "Simple configuration management with python."
9
9
  authors = [ "pwwang <pwwang@pwwang.com>",]
10
10
  license = "MIT"
@@ -20,33 +20,37 @@ python = "^3.9"
20
20
  diot = "^0.3.2"
21
21
  python-dotenv = { version="^1.1", optional = true}
22
22
  pyyaml = { version="^6", optional = true}
23
+ panpath = "^0.4.8"
23
24
  # Use rtoml only when the wheel is available (linux)
24
25
  rtoml = {version = "^0.12", optional = true, platform = "linux"}
25
26
  tomli = {version = "^2.0", optional = true, markers = 'sys_platform != "linux"'}
26
27
  iniconfig = {version = "^2.0", optional = true}
27
- yunpath = {version = "^0.0.4", optional = true}
28
+ aiofiles = {version = ">=23.0.0", optional = true}
28
29
 
29
30
  [tool.poetry.extras]
30
31
  ini = [ "iniconfig" ]
31
32
  env = [ "python-dotenv"]
32
33
  yaml = [ "pyyaml"]
33
34
  toml = [ "rtoml", "tomli"]
35
+ async = [ "aiofiles"]
34
36
  all = [ "iniconfig", "python-dotenv", "pyyaml", "rtoml", "tomli"]
35
- cloud = [ "yunpath"]
36
37
 
37
38
  [tool.poetry.group.dev.dependencies]
38
39
  pytest = "^8"
39
40
  pytest-cov = "^6"
41
+ pytest-asyncio = "^1"
42
+ backports-asyncio-runner = {version = "^1.1.0", python = "<3.11"}
40
43
 
41
44
  [tool.pytest.ini_options]
42
- addopts = "-vv -p no:asyncio --cov=simpleconf --cov-report xml:cov.xml --cov-report term-missing"
45
+ addopts = "-vv --cov=simpleconf --cov-report xml:cov.xml --cov-report term-missing"
43
46
  filterwarnings = [
44
47
  "error"
45
48
  ]
46
49
  console_output_style = "progress"
47
50
  junit_family = "xunit1"
51
+ asyncio_mode = "auto"
48
52
 
49
53
  [tool.black]
50
54
  line-length = 88
51
- target-version = ['py37', 'py38', 'py39', 'py310']
55
+ target-version = ['py39', 'py310', 'py311', 'py312']
52
56
  include = '\.pyi?$'
@@ -1,3 +1,3 @@
1
1
  from .config import Config, ProfileConfig
2
2
 
3
- __version__ = "0.7.2"
3
+ __version__ = "0.8.0"
@@ -14,8 +14,9 @@ LoaderType = Union[str, Loader, None]
14
14
  class Config:
15
15
  """The configuration class"""
16
16
 
17
- @staticmethod
17
+ @classmethod
18
18
  def load(
19
+ cls,
19
20
  *configs: Any,
20
21
  loader: LoaderType | Sequence[LoaderType] = None,
21
22
  ignore_nonexist: bool = False,
@@ -50,8 +51,51 @@ class Config:
50
51
 
51
52
  return out
52
53
 
53
- @staticmethod
54
+ @classmethod
55
+ async def a_load(
56
+ cls,
57
+ *configs: Any,
58
+ loader: LoaderType | Sequence[LoaderType] = None,
59
+ ignore_nonexist: bool = False,
60
+ ) -> Diot:
61
+ """Asynchronously load the configuration from the files, or other
62
+ configurations
63
+
64
+ Args:
65
+ *configs: The configuration files or other configurations to load
66
+ Latter ones will override the former ones for items with the
67
+ same keys recursively.
68
+ loader: The loader to use. If a list is given, it must have the
69
+ same length as configs.
70
+ ignore_nonexist: Whether to ignore non-existent files
71
+ Otherwise, will raise errors
72
+
73
+ Returns:
74
+ A Diot object with the loaded configurations
75
+ """
76
+ if not isinstance(loader, Sequence) or isinstance(loader, str):
77
+ loader = [loader] * len(configs)
78
+
79
+ if len(loader) != len(configs):
80
+ raise ValueError(
81
+ f"Length of loader ({len(loader)}) does not match "
82
+ f"length of configs ({len(configs)})"
83
+ )
84
+
85
+ out = Diot()
86
+ for i, conf in enumerate(configs):
87
+ loaded = await cls.a_load_one(
88
+ conf,
89
+ loader[i],
90
+ ignore_nonexist,
91
+ )
92
+ out.update_recursively(loaded)
93
+
94
+ return out
95
+
96
+ @classmethod
54
97
  def load_one(
98
+ cls,
55
99
  config,
56
100
  loader: str | Loader | None = None,
57
101
  ignore_nonexist: bool = False,
@@ -78,12 +122,42 @@ class Config:
78
122
 
79
123
  return loader.load(config, ignore_nonexist)
80
124
 
125
+ @classmethod
126
+ async def a_load_one(
127
+ cls,
128
+ config,
129
+ loader: str | Loader | None = None,
130
+ ignore_nonexist: bool = False,
131
+ ) -> Diot:
132
+ """Asynchronously load the configuration from the file
133
+
134
+ Args:
135
+ config: The configuration file to load
136
+ loader: The loader to use
137
+ ignore_nonexist: Whether to ignore non-existent files
138
+ Otherwise, will raise errors
139
+
140
+ Returns:
141
+ A Diot object with the loaded configuration
142
+ """
143
+ if loader is None:
144
+ if hasattr(config, "read"):
145
+ raise ValueError("'loader' must be specified for stream")
146
+
147
+ ext = config_to_ext(config)
148
+ loader = get_loader(ext)
149
+ else:
150
+ loader = get_loader(loader)
151
+
152
+ return await loader.a_load(config, ignore_nonexist)
153
+
81
154
 
82
155
  class ProfileConfig:
83
156
  """The configuration class with profile support"""
84
157
 
85
- @staticmethod
158
+ @classmethod
86
159
  def load(
160
+ cls,
87
161
  *configs: Any,
88
162
  loader: LoaderType | Sequence[LoaderType] = None,
89
163
  ignore_nonexist: bool = False,
@@ -146,8 +220,78 @@ class ProfileConfig:
146
220
 
147
221
  return out
148
222
 
149
- @staticmethod
223
+ @classmethod
224
+ async def a_load(
225
+ cls,
226
+ *configs: Any,
227
+ loader: LoaderType | Sequence[LoaderType] = None,
228
+ ignore_nonexist: bool = False,
229
+ base: str = "default",
230
+ allow_missing_base: bool = False,
231
+ ) -> Diot:
232
+ """Asynchronously load the configuration from the files, or other
233
+ configurations
234
+
235
+ Args:
236
+ *configs: The configuration files or other configurations to load
237
+ Latter ones will override the former ones for items with the
238
+ same profile and keys recursively.
239
+ loader: The loader to use. If a list is given, it must have the
240
+ same length as configs.
241
+ ignore_nonexist: Whether to ignore non-existent files
242
+ Otherwise, will raise errors
243
+ base: The default profile to use after loading
244
+ allow_missing_base: Whether to allow missing base profile
245
+ If False, will raise errors when the base profile is not found
246
+ in the loaded profiles.
247
+
248
+ Returns:
249
+ A Diot object with the loaded configurations
250
+ """
251
+ if not isinstance(loader, Sequence) or isinstance(loader, str):
252
+ loader = [loader] * len(configs)
253
+
254
+ if len(loader) != len(configs):
255
+ raise ValueError(
256
+ f"Length of loader ({len(loader)}) does not match "
257
+ f"length of configs ({len(configs)})"
258
+ )
259
+
260
+ out = Diot({POOL_KEY: Diot()})
261
+ pool = out[POOL_KEY]
262
+ out[META_KEY] = {
263
+ "current_profile": None,
264
+ "base_profile": None,
265
+ }
266
+ for i, conf in enumerate(configs):
267
+ lder = loader[i]
268
+
269
+ if lder is None and hasattr(conf, "read"):
270
+ raise ValueError("'loader' must be specified for stream")
271
+
272
+ if lder is None:
273
+ ext = config_to_ext(conf)
274
+ lder = get_loader(ext)
275
+ else:
276
+ lder = get_loader(lder)
277
+
278
+ loaded = await lder.a_load_with_profiles(conf, ignore_nonexist)
279
+ for profile, value in loaded.items():
280
+ profile = profile.lower()
281
+ pool.setdefault(profile, Diot())
282
+ pool[profile].update_recursively(value)
283
+
284
+ if base and base not in pool and not allow_missing_base:
285
+ raise ValueError(f"Base profile '{base}' not found")
286
+
287
+ if base and base in pool:
288
+ out = ProfileConfig.use_profile(out, base, base=base)
289
+
290
+ return out
291
+
292
+ @classmethod
150
293
  def load_one(
294
+ cls,
151
295
  conf: Any,
152
296
  loader: str | Loader | None = None,
153
297
  ignore_nonexist: bool = False,
@@ -197,6 +341,61 @@ class ProfileConfig:
197
341
 
198
342
  return out
199
343
 
344
+ @classmethod
345
+ async def a_load_one(
346
+ cls,
347
+ conf: Any,
348
+ loader: str | Loader | None = None,
349
+ ignore_nonexist: bool = False,
350
+ base: str = "default",
351
+ allow_missing_base: bool = False,
352
+ ) -> Diot:
353
+ """Asynchronously load the configuration from the file
354
+
355
+ Args:
356
+ conf: The configuration file to load
357
+ loader: The loader to use. Will detect from conf by default
358
+ ignore_nonexist: Whether to ignore non-existent files
359
+ Otherwise, will raise errors
360
+ base: The default profile to use after loading
361
+ allow_missing_base: Whether to allow missing base profile
362
+ If False, will raise errors when the base profile is not found
363
+ in the loaded profiles.
364
+
365
+ Returns:
366
+ A Diot object with the loaded configuration
367
+ """
368
+
369
+ out = Diot({POOL_KEY: Diot()})
370
+ pool = out[POOL_KEY]
371
+ out[META_KEY] = {
372
+ "current_profile": None,
373
+ "base_profile": None,
374
+ }
375
+
376
+ if loader is None:
377
+ if hasattr(conf, "read"):
378
+ raise ValueError("'loader' must be specified for stream")
379
+
380
+ ext = config_to_ext(conf)
381
+ loader = get_loader(ext)
382
+ else:
383
+ loader = get_loader(loader)
384
+
385
+ loaded = await loader.a_load_with_profiles(conf, ignore_nonexist)
386
+ for profile, value in loaded.items():
387
+ profile = profile.lower()
388
+ pool.setdefault(profile, Diot())
389
+ pool[profile].update_recursively(value)
390
+
391
+ if base and base not in pool and not allow_missing_base:
392
+ raise ValueError(f"Base profile '{base}' not found")
393
+
394
+ if base and base in pool:
395
+ out = ProfileConfig.use_profile(out, base, base=base)
396
+
397
+ return out
398
+
200
399
  @staticmethod
201
400
  def use_profile(
202
401
  conf: Diot,
@@ -0,0 +1,134 @@
1
+ from __future__ import annotations
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import Any, Callable, List, Dict
5
+ from pathlib import Path
6
+
7
+ from diot import Diot
8
+ from panpath import PanPath
9
+ from ..caster import cast
10
+
11
+
12
+ class Loader(ABC):
13
+
14
+ CASTERS: List[Callable[[str, bool], Any]] | None = None
15
+
16
+ @staticmethod
17
+ def _convert_path(conf: str | Path) -> Path:
18
+ """Convert the conf to Path if it is a string"""
19
+ if isinstance(conf, (str, Path)):
20
+ return PanPath(conf)
21
+ return conf
22
+
23
+ @abstractmethod
24
+ def loading(self, conf: Any, ignore_nonexist: bool) -> Dict[str, Any]:
25
+ """Load the configuration from the path or configurations"""
26
+
27
+ @abstractmethod
28
+ async def a_loading(self, conf: Any, ignore_nonexist: bool) -> Dict[str, Any]:
29
+ """Asynchronously load the configuration from the path or configurations"""
30
+
31
+ @classmethod
32
+ def _convert(cls, conf: Any, loaded: Any) -> Diot:
33
+ """Convert the loaded configuration to Diot"""
34
+ if cls.CASTERS:
35
+ loaded = cast(loaded, cls.CASTERS)
36
+
37
+ return Diot(loaded)
38
+
39
+ @classmethod
40
+ def _convert_with_profiles(cls, conf: Any, loaded: Any) -> Diot:
41
+ """Convert the loaded configuration with profiles to Diot"""
42
+ return Diot(loaded)
43
+
44
+ def _exists(self, conf: str | Path, ignore_exist: bool) -> bool:
45
+ """Check if the configuration file exists"""
46
+ path = self.__class__._convert_path(conf)
47
+ exists = path.exists()
48
+ if not ignore_exist and not exists:
49
+ raise FileNotFoundError(f"{conf} does not exist")
50
+ return exists
51
+
52
+ async def _a_exists(self, conf: str | Path, ignore_exist: bool) -> bool:
53
+ """Asynchronously check if the configuration file exists"""
54
+ path = self.__class__._convert_path(conf)
55
+ exists = await path.a_exists()
56
+ if not ignore_exist and not exists:
57
+ raise FileNotFoundError(f"{conf} does not exist")
58
+ return exists
59
+
60
+ def load(self, conf: Any, ignore_nonexist: bool = False) -> Diot:
61
+ """Load the configuration from the path or configurations and cast
62
+ values
63
+
64
+ Args:
65
+ conf: The configuration file to load
66
+
67
+ Returns:
68
+ The Diot object
69
+ """
70
+ path = self.__class__._convert_path(conf)
71
+ loaded = self.loading(path, ignore_nonexist)
72
+ return self.__class__._convert(conf, loaded)
73
+
74
+ async def a_load(self, conf: Any, ignore_nonexist: bool = False) -> Diot:
75
+ """Asynchronously load the configuration from the path or configurations
76
+ and cast values
77
+
78
+ Args:
79
+ conf: The configuration file to load
80
+
81
+ Returns:
82
+ The Diot object
83
+ """
84
+ path = self.__class__._convert_path(conf)
85
+ loaded = await self.a_loading(path, ignore_nonexist)
86
+ return self.__class__._convert(conf, loaded)
87
+
88
+ def load_with_profiles( # type: ignore[override]
89
+ self,
90
+ conf: Any,
91
+ ignore_nonexist: bool = False,
92
+ ) -> Diot:
93
+ """Load the configuration from the path or configurations with profiles
94
+ and cast values
95
+
96
+ Args:
97
+ conf: The configuration file to load
98
+
99
+ Returns:
100
+ The Diot object
101
+ """
102
+ path = self.__class__._convert_path(conf)
103
+ loaded = self.loading(path, ignore_nonexist)
104
+ return self.__class__._convert_with_profiles(conf, loaded)
105
+
106
+ async def a_load_with_profiles( # type: ignore[override]
107
+ self,
108
+ conf: Any,
109
+ ignore_nonexist: bool = False,
110
+ ) -> Diot:
111
+ """Asynchronously load the configuration from the path or configurations
112
+ with profiles and cast values
113
+
114
+ Args:
115
+ conf: The configuration file to load
116
+
117
+ Returns:
118
+ The Diot object
119
+ """
120
+ path = self.__class__._convert_path(conf)
121
+ loaded = await self.a_loading(path, ignore_nonexist)
122
+ return self.__class__._convert_with_profiles(conf, loaded)
123
+
124
+
125
+ class NoConvertingPathMixin(ABC):
126
+ """String loader base class"""
127
+
128
+ @staticmethod
129
+ def _convert_path(conf: str) -> str:
130
+ return conf
131
+
132
+ async def a_loading(self, conf: Any, ignore_nonexist: bool) -> Dict[str, Any]:
133
+ """Asynchronously load the configuration from a toml file"""
134
+ return self.loading(conf, ignore_nonexist)
@@ -14,6 +14,10 @@ class DictLoader(Loader):
14
14
  """Load the configuration from a dict"""
15
15
  return conf
16
16
 
17
+ async def a_loading(self, conf: Any, ignore_nonexist: bool) -> Dict[str, Any]:
18
+ """Asynchronously load the configuration from a dict"""
19
+ return conf
20
+
17
21
 
18
22
  class DictsLoader(NoConvertingPathMixin, DictLoader):
19
23
  """Dict string loader"""
@@ -1,7 +1,7 @@
1
1
  import warnings
2
2
  import io
3
3
  from pathlib import Path
4
- from typing import Any, Dict
4
+ from typing import Any, Awaitable, Dict
5
5
  from diot import Diot
6
6
 
7
7
  from ..utils import require_package
@@ -46,25 +46,37 @@ class EnvLoader(Loader):
46
46
 
47
47
  return dotenv.main.DotEnv(conf).dict()
48
48
 
49
- def load_with_profiles( # type: ignore[override]
50
- self,
49
+ async def a_loading(self, conf, ignore_nonexist):
50
+ """Asynchronously load the configuration from a .env file"""
51
+ if hasattr(conf, "read"):
52
+ content = conf.read()
53
+ if isinstance(content, Awaitable):
54
+ content = await content
55
+ if isinstance(content, bytes):
56
+ content = content.decode()
57
+ return dotenv.dotenv_values(stream=io.StringIO(content))
58
+
59
+ if not await self._a_exists(conf, ignore_nonexist):
60
+ return {}
61
+
62
+ return dotenv.main.DotEnv(conf).dict()
63
+
64
+ @classmethod
65
+ def _convert_with_profiles( # type: ignore[override]
66
+ cls,
51
67
  conf: Any,
52
- ignore_nonexist: bool = False,
68
+ loaded: Dict[str, Any],
53
69
  ) -> Diot:
54
- """Load and cast the configuration from a .env file with profiles"""
55
- envs = self.loading(conf, ignore_nonexist)
56
70
  out = Diot()
57
- for k, v in envs.items():
71
+ for k, v in loaded.items():
58
72
  if "_" not in k:
59
- warnings.warn(
60
- f"{Path(conf).name}: No profile name found in key: {k}"
61
- )
73
+ warnings.warn(f"{Path(conf).name}: No profile name found in key: {k}")
62
74
  continue
63
75
  profile, key = k.split("_", 1)
64
76
  profile = profile.lower()
65
77
  out.setdefault(profile, Diot())[key] = v
66
78
 
67
- return cast(out, self.__class__.CASTERS)
79
+ return cast(out, cls.CASTERS)
68
80
 
69
81
 
70
82
  class EnvsLoader(NoConvertingPathMixin, EnvLoader):
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import warnings
4
- from typing import Any, Dict
4
+ from typing import Any, Awaitable, Dict
5
5
  from pathlib import Path
6
6
  from diot import Diot
7
7
 
@@ -47,10 +47,28 @@ class IniLoader(Loader):
47
47
 
48
48
  return iniconfig.IniConfig(conf).sections
49
49
 
50
- def load(self, conf: Any, ignore_nonexist: bool = False) -> Diot:
51
- """Load and cast the configuration from an ini-like file"""
52
- sections = self.loading(conf, ignore_nonexist)
53
- keys = list(sections)
50
+ async def a_loading(self, conf: Any, ignore_nonexist: bool) -> Dict[str, Any]:
51
+ """Asynchronously load the configuration from an ini-like file"""
52
+ if hasattr(conf, "read"):
53
+ content = conf.read()
54
+ if isinstance(content, Awaitable):
55
+ content = await content
56
+ if isinstance(content, bytes):
57
+ content = content.decode()
58
+ return iniconfig.IniConfig("<config>", content).sections
59
+
60
+ if not await self._a_exists(conf, ignore_nonexist):
61
+ return {"default": {}}
62
+
63
+ return iniconfig.IniConfig(conf).sections
64
+
65
+ @classmethod
66
+ def _convert( # type: ignore[override]
67
+ cls,
68
+ conf: Any,
69
+ loaded: Dict[str, Any],
70
+ ) -> Diot:
71
+ keys = list(loaded)
54
72
 
55
73
  if hasattr(conf, "read"):
56
74
  pathname = "<config>"
@@ -66,22 +84,19 @@ class IniLoader(Loader):
66
84
  )
67
85
 
68
86
  if len(keys) == 0 or keys[0].lower() != "default":
69
- raise ValueError(
70
- f"{pathname}: Only the default section can be loaded."
71
- )
87
+ raise ValueError(f"{pathname}: Only the default section can be loaded.")
72
88
 
73
- return cast(Diot(sections[keys[0]]), self.__class__.CASTERS)
89
+ return cast(Diot(loaded[keys[0]]), cls.CASTERS)
74
90
 
75
- def load_with_profiles( # type: ignore[override]
76
- self,
91
+ @classmethod
92
+ def _convert_with_profiles( # type: ignore[override]
93
+ cls,
77
94
  conf: Any,
78
- ignore_nonexist: bool = False,
95
+ loaded: Dict[str, Any],
79
96
  ) -> Diot:
80
- """Load and cast the configuration from an ini-like file with profiles"""
81
- sections = self.loading(conf, ignore_nonexist)
82
97
  out = Diot()
83
- for k, v in sections.items():
84
- out[k.lower()] = cast(v, self.__class__.CASTERS)
98
+ for k, v in loaded.items():
99
+ out[k.lower()] = cast(v, cls.CASTERS)
85
100
  return out
86
101
 
87
102
 
@@ -0,0 +1,47 @@
1
+ import json
2
+ from typing import Any, Awaitable, Dict
3
+
4
+ from . import Loader, NoConvertingPathMixin
5
+
6
+
7
+ class JsonLoader(Loader):
8
+ """Json file loader"""
9
+
10
+ def loading(self, conf: Any, ignore_nonexist: bool) -> Dict[str, Any]:
11
+ """Load the configuration from a json file"""
12
+ if hasattr(conf, "read"):
13
+ content = conf.read()
14
+ return json.loads(content)
15
+
16
+ if not self._exists(conf, ignore_nonexist):
17
+ return {}
18
+
19
+ with open(conf) as f:
20
+ return json.load(f)
21
+
22
+ async def a_loading(self, conf: Any, ignore_nonexist: bool) -> Dict[str, Any]:
23
+ """Asynchronously load the configuration from a json file"""
24
+ if hasattr(conf, "read"):
25
+ content = conf.read()
26
+ if isinstance(content, Awaitable):
27
+ content = await content
28
+ if isinstance(content, bytes):
29
+ content = content.decode()
30
+ return json.loads(content)
31
+
32
+ if not await self._a_exists(conf, ignore_nonexist):
33
+ return {}
34
+
35
+ async with self.__class__._convert_path(conf).a_open() as f:
36
+ content = await f.read()
37
+ if isinstance(content, bytes): # pragma: no cover
38
+ content = content.decode()
39
+ return json.loads(content)
40
+
41
+
42
+ class JsonsLoader(NoConvertingPathMixin, JsonLoader):
43
+ """Json string loader"""
44
+
45
+ def loading(self, conf: Any, ignore_nonexist: bool) -> Dict[str, Any]:
46
+ """Load the configuration from a json file"""
47
+ return json.loads(conf)
@@ -1,6 +1,6 @@
1
+ import warnings
1
2
  from os import environ
2
3
  from typing import Any, Dict
3
- import warnings
4
4
 
5
5
  from diot import Diot
6
6
 
@@ -42,23 +42,27 @@ class OsenvLoader(NoConvertingPathMixin, Loader):
42
42
  out[k[len_prefix:]] = v
43
43
  return out
44
44
 
45
- def load_with_profiles( # type: ignore[override]
45
+ async def a_loading(
46
46
  self,
47
47
  conf: Any,
48
48
  ignore_nonexist: bool = False,
49
+ ) -> Dict[str, Any]:
50
+ """Asynchronously load the configuration from environment variables"""
51
+ return self.loading(conf, ignore_nonexist)
52
+
53
+ @classmethod
54
+ def _convert_with_profiles( # type: ignore[override]
55
+ cls,
56
+ conf: Any,
57
+ loaded: Dict[str, Any],
49
58
  ) -> Diot:
50
- prefix = f"{conf[:-6]}_" if len(conf) > 6 else ""
51
- len_prefix = len(prefix)
52
59
  out = Diot()
53
- for k, v in environ.items():
54
- if not k.startswith(prefix):
55
- continue
56
- key = k[len_prefix:]
60
+ for key, val in loaded.items():
57
61
  if "_" not in key:
58
- warnings.warn(f"{conf}: No profile name found in key: {k}")
62
+ warnings.warn(f"{conf}: No profile name found in key: {key}")
59
63
  continue
60
64
  profile, key = key.split("_", 1)
61
65
  profile = profile.lower()
62
- out.setdefault(profile, Diot())[key] = v
66
+ out.setdefault(profile, Diot())[key] = val
63
67
 
64
- return cast(out, self.__class__.CASTERS)
68
+ return cast(out, cls.CASTERS)
@@ -1,4 +1,4 @@
1
- from typing import Any, Dict
1
+ from typing import Any, Dict, Awaitable
2
2
 
3
3
  from ..utils import require_package
4
4
  from ..caster import (
@@ -34,6 +34,27 @@ class TomlLoader(Loader):
34
34
  with open(conf, "r") as f: # rtoml
35
35
  return toml.load(f)
36
36
 
37
+ async def a_loading(self, conf: Any, ignore_nonexist: bool) -> Dict[str, Any]:
38
+ """Asynchronously load the configuration from a toml file"""
39
+ if hasattr(conf, "read"):
40
+ content = conf.read()
41
+ if isinstance(content, Awaitable):
42
+ content = await content
43
+ if isinstance(content, bytes):
44
+ content = content.decode()
45
+ return toml.loads(content)
46
+
47
+ if not await self._a_exists(conf, ignore_nonexist):
48
+ return {}
49
+
50
+ async with self.__class__._convert_path(conf).a_open("rb") as f:
51
+ content = await f.read()
52
+ try:
53
+ return toml.loads(content)
54
+ except TypeError:
55
+ content = content.decode()
56
+ return toml.loads(content)
57
+
37
58
 
38
59
  class TomlsLoader(NoConvertingPathMixin, TomlLoader):
39
60
  """Toml string loader"""
@@ -1,4 +1,4 @@
1
- from typing import Any, Dict
1
+ from typing import Any, Dict, Awaitable
2
2
 
3
3
  from . import Loader, NoConvertingPathMixin
4
4
  from ..utils import require_package
@@ -21,6 +21,25 @@ class YamlLoader(Loader):
21
21
  with open(conf) as f:
22
22
  return yaml.load(f, Loader=yaml.FullLoader)
23
23
 
24
+ async def a_loading(self, conf: Any, ignore_nonexist: bool) -> Dict[str, Any]:
25
+ """Asynchronously load the configuration from a yaml file"""
26
+ if hasattr(conf, "read"):
27
+ content = conf.read()
28
+ if isinstance(content, Awaitable):
29
+ content = await content
30
+ if isinstance(content, bytes):
31
+ content = content.decode()
32
+ return yaml.load(content, Loader=yaml.FullLoader)
33
+
34
+ if not await self._a_exists(conf, ignore_nonexist):
35
+ return {}
36
+
37
+ async with self.__class__._convert_path(conf).a_open() as f:
38
+ content = await f.read()
39
+ if isinstance(content, bytes): # pragma: no cover
40
+ content = content.decode()
41
+ return yaml.load(content, Loader=yaml.FullLoader)
42
+
24
43
 
25
44
  class YamlsLoader(NoConvertingPathMixin, YamlLoader):
26
45
  """Yaml string loader"""
@@ -1,63 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from abc import ABC, abstractmethod
4
- from typing import Any, Callable, List, Dict
5
- from pathlib import Path
6
-
7
- from diot import Diot
8
- from ..caster import cast
9
-
10
-
11
- class Loader(ABC):
12
-
13
- CASTERS: List[Callable[[str, bool], Any]] | None = None
14
-
15
- @staticmethod
16
- def _convert_path(conf: str | Path) -> Path:
17
- """Convert the conf to Path if it is a string"""
18
- try:
19
- from yunpath import AnyPath
20
- except ImportError:
21
- AnyPath = Path
22
-
23
- if isinstance(conf, str):
24
- return AnyPath(conf)
25
- return conf
26
-
27
- @abstractmethod
28
- def loading(self, conf: Any, ignore_nonexist: bool) -> Dict[str, Any]:
29
- """Load the configuration from the path or configurations"""
30
-
31
- def _exists(self, conf: str | Path, ignore_exist: bool) -> bool:
32
- """Check if the configuration file exists"""
33
- path = self.__class__._convert_path(conf)
34
- if not ignore_exist and not path.exists():
35
- raise FileNotFoundError(f"{conf} does not exist")
36
- return path.exists()
37
-
38
- def load(self, conf: Any, ignore_nonexist: bool = False) -> Diot:
39
- """Load the configuration from the path or configurations and cast
40
- values
41
-
42
- Args:
43
- conf: The configuration file to load
44
-
45
- Returns:
46
- The Diot object
47
- """
48
- path = self.__class__._convert_path(conf)
49
- loaded = self.loading(path, ignore_nonexist)
50
- if self.__class__.CASTERS:
51
- loaded = cast(loaded, self.__class__.CASTERS)
52
-
53
- return Diot(loaded)
54
-
55
- load_with_profiles = load
56
-
57
-
58
- class NoConvertingPathMixin(ABC):
59
- """String loader base class"""
60
-
61
- @staticmethod
62
- def _convert_path(conf: str) -> str:
63
- return conf
@@ -1,28 +0,0 @@
1
- import json
2
- from typing import Any, Dict
3
-
4
- from . import Loader, NoConvertingPathMixin
5
-
6
-
7
- class JsonLoader(Loader):
8
- """Json file loader"""
9
-
10
- def loading(self, conf: Any, ignore_nonexist: bool) -> Dict[str, Any]:
11
- """Load the configuration from a json file"""
12
- if hasattr(conf, "read"):
13
- content = conf.read()
14
- return json.loads(content)
15
-
16
- if not self._exists(conf, ignore_nonexist):
17
- return {}
18
-
19
- with open(conf) as f:
20
- return json.load(f)
21
-
22
-
23
- class JsonsLoader(NoConvertingPathMixin, JsonLoader):
24
- """Json string loader"""
25
-
26
- def loading(self, conf: Any, ignore_nonexist: bool) -> Dict[str, Any]:
27
- """Load the configuration from a json file"""
28
- return json.loads(conf)