Skip to content

Commit 6316f83

Browse files
Initial commit
0 parents  commit 6316f83

File tree

7 files changed

+327
-0
lines changed

7 files changed

+327
-0
lines changed

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
__pycache__/
2+
*venv*/
3+
.pytest_cache
4+
.ruff_cache
5+
.mypy_cache
6+
.coverage
7+
.env
8+
config.yaml
9+
.test.env
10+
*.log

.python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.8

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2024 shadowy-pycoder
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.

README.md

Whitespace-only changes.

pyproject.toml

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
[project]
2+
name = "pyya"
3+
version = "0.1.0"
4+
authors = [
5+
{ name="shadowy-pycoder", email="[email protected]" },
6+
]
7+
description = "Convert YAML configuration files to Python objects"
8+
readme = "README.md"
9+
requires-python = ">=3.8"
10+
classifiers = [
11+
"Programming Language :: Python :: 3",
12+
"Programming Language :: Python :: 3.8",
13+
"Programming Language :: Python :: 3.9",
14+
"Programming Language :: Python :: 3.10",
15+
"Programming Language :: Python :: 3.11",
16+
"Programming Language :: Python :: 3.12",
17+
"Programming Language :: Python :: 3.13",
18+
"License :: OSI Approved :: MIT License",
19+
"Operating System :: MacOS :: MacOS X",
20+
"Operating System :: Microsoft :: Windows",
21+
"Operating System :: POSIX :: Linux",
22+
]
23+
dependencies = [
24+
"camel-converter>=3.1.2",
25+
"munch>=4.0.0",
26+
"pyyaml>=6.0.2",
27+
"types-pyyaml>=6.0.12.20240917",
28+
]
29+
[project.urls]
30+
Homepage = "https://github.com/shadowy-pycoder/pyya"
31+
Repository = "https://github.com/shadowy-pycoder/pyya"
32+
Issues = "https://github.com/shadowy-pycoder/pyya/issues"
33+
34+
[tool.mypy]
35+
python_version = "3.8"
36+
cache_dir = ".mypy_cache/strict"
37+
allow_redefinition = true
38+
strict_optional = false
39+
show_error_codes = true
40+
show_column_numbers = true
41+
warn_no_return = true
42+
disallow_any_unimported = false
43+
warn_unused_configs = true
44+
disallow_any_generics = true
45+
disallow_subclassing_any = true
46+
disallow_untyped_calls = false
47+
disallow_untyped_defs = true
48+
disallow_incomplete_defs = true
49+
check_untyped_defs = true
50+
disallow_untyped_decorators = true
51+
no_implicit_optional = true
52+
implicit_reexport = true
53+
strict_equality = true
54+
ignore_missing_imports = true
55+
56+
[tool.pytest.ini_options]
57+
pythonpath = "."
58+
filterwarnings = [
59+
"ignore::DeprecationWarning",
60+
"ignore::PendingDeprecationWarning",
61+
"ignore::ImportWarning",
62+
"ignore::ResourceWarning"
63+
]
64+
asyncio_mode = "auto"
65+
env_files = ".test.env"
66+
env_override_existing_values = true
67+
68+
[tool.ruff]
69+
exclude = [
70+
".bzr",
71+
".direnv",
72+
".eggs",
73+
".git",
74+
".git-rewrite",
75+
".hg",
76+
".ipynb_checkpoints",
77+
".mypy_cache",
78+
".nox",
79+
".pants.d",
80+
".pyenv",
81+
".pytest_cache",
82+
".pytype",
83+
".ruff_cache",
84+
".svn",
85+
".tox",
86+
".venv",
87+
".vscode",
88+
"__pypackages__",
89+
"_build",
90+
"buck-out",
91+
"build",
92+
"dist",
93+
"node_modules",
94+
"site-packages",
95+
"venv",
96+
]
97+
line-length = 120
98+
indent-width = 4
99+
target-version = "py38"
100+
101+
[tool.ruff.format]
102+
quote-style = "single"
103+
indent-style = "space"
104+
skip-magic-trailing-comma = false
105+
line-ending = "auto"
106+
107+
[tool.ruff.lint]
108+
select = ["E1", "E2", "E4", "E7", "E9", "F", "D3"]
109+
ignore = ["ANN201", "Q000", "D102", "D103"]
110+
fixable = ["ALL"]
111+
unfixable = []
112+
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
113+
114+
[tool.ruff.lint.isort]
115+
lines-after-imports = 2

pyya/__init__.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import keyword
2+
import logging
3+
from pathlib import Path
4+
from typing import Any, Dict, Optional, Union
5+
6+
import yaml
7+
from camel_converter import to_snake
8+
from munch import Munch, munchify
9+
10+
11+
logging.basicConfig(format='%(asctime)-15s \t%(levelname)-8s \t%(name)-8s \t%(message)s')
12+
logger = logging.getLogger('paya')
13+
14+
15+
ConfigType = Dict[str, Any]
16+
17+
18+
class PayaError(RuntimeError): ...
19+
20+
21+
def init_config(
22+
config: Union[str, Path] = 'config.yaml',
23+
default_config: Union[str, Path] = 'default.config.yaml',
24+
*,
25+
convert_keys_to_snake_case: bool = False,
26+
check_for_identifiers: bool = False,
27+
) -> Munch:
28+
def _merge_configs(_raw_data: ConfigType, _default_raw_data: ConfigType) -> None:
29+
for section, entry in _default_raw_data.items():
30+
if section not in _raw_data or _raw_data[section] is None:
31+
section = _sanitize_section(section)
32+
_raw_data[section] = entry
33+
logger.warning(f'section `{section}` with value `{entry}` taken from {default_config}')
34+
elif isinstance(entry, dict):
35+
section = _sanitize_section(section)
36+
_merge_configs(_raw_data[section], entry)
37+
38+
def _sanitize_section(section: str) -> str:
39+
if convert_keys_to_snake_case:
40+
logger.warning(f'converting section `{section}` to snake case')
41+
section = to_snake(section)
42+
if check_for_identifiers:
43+
if not section.isidentifier():
44+
err_msg = f'section `{section}` is not a valid identifier, aborting'
45+
logger.error(err_msg)
46+
raise PayaError(err_msg)
47+
if keyword.iskeyword(section):
48+
logger.warning(f'section `{section}` is a keyword, renaming to `_{section}`')
49+
section = f'_{section}'
50+
return section
51+
52+
try:
53+
with open(Path(default_config)) as fstream:
54+
_default_raw_data: Optional[ConfigType] = yaml.safe_load(fstream)
55+
if _default_raw_data is None:
56+
raise FileNotFoundError()
57+
except FileNotFoundError as e:
58+
logger.error(e)
59+
raise PayaError(f'{default_config} file is missing or empty') from None
60+
try:
61+
with open(Path(config)) as fstream:
62+
_raw_data: ConfigType = yaml.safe_load(fstream) or {}
63+
except FileNotFoundError:
64+
logger.warning(f'{config} file not found, using {default_config}')
65+
_raw_data = {}
66+
_merge_configs(_raw_data, _default_raw_data)
67+
try:
68+
return munchify(_raw_data)
69+
except Exception as e:
70+
message = f'{default_config} file is corrupted: {repr(e)}'
71+
logger.error(message)
72+
raise PayaError(message) from None

0 commit comments

Comments
 (0)