masha 0.0.0__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.
- masha/__init__.py +14 -0
- masha/cli.py +237 -0
- masha/config_loader.py +175 -0
- masha/config_validator.py +161 -0
- masha/env_loader.py +86 -0
- masha/filters/string_utils.py +29 -0
- masha/logger_factory.py +38 -0
- masha/logging.conf +28 -0
- masha/template_renderer.py +127 -0
- masha/tests/maths_utils.py +29 -0
- masha/version.py +5 -0
- masha-0.0.0.dist-info/LICENSE +201 -0
- masha-0.0.0.dist-info/METADATA +191 -0
- masha-0.0.0.dist-info/RECORD +16 -0
- masha-0.0.0.dist-info/WHEEL +4 -0
- masha-0.0.0.dist-info/entry_points.txt +3 -0
masha/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# pylint: disable=E0605,C0114
|
|
2
|
+
# ruff: noqa: F401
|
|
3
|
+
from .config_loader import load_and_merge_configs, load_config, merge_configs
|
|
4
|
+
from .config_validator import load_model_class, validate_config
|
|
5
|
+
from .env_loader import resolve_env_variables
|
|
6
|
+
from .logger_factory import create_logger
|
|
7
|
+
from .template_renderer import (
|
|
8
|
+
load_functions_from_directory,
|
|
9
|
+
load_functions_from_file,
|
|
10
|
+
render_templates_with_filters,
|
|
11
|
+
)
|
|
12
|
+
from .version import __version__
|
|
13
|
+
|
|
14
|
+
__all__ = "masha"
|
masha/cli.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Render the input file using Jinja2 with the provided configuration.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Dict
|
|
9
|
+
|
|
10
|
+
import click
|
|
11
|
+
import jinja2
|
|
12
|
+
from returns.result import Failure, Result, Success
|
|
13
|
+
|
|
14
|
+
# pylint: disable=W1203
|
|
15
|
+
from masha.config_loader import load_and_merge_configs
|
|
16
|
+
from masha.config_validator import load_model_class, validate_config
|
|
17
|
+
from masha.env_loader import resolve_env_variables
|
|
18
|
+
from masha.logger_factory import create_logger
|
|
19
|
+
from masha.template_renderer import (
|
|
20
|
+
load_functions_from_directory,
|
|
21
|
+
render_templates_with_filters,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
logger = create_logger("masha")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def render_jinja_template(
|
|
28
|
+
input_file: Path,
|
|
29
|
+
output_file: Path,
|
|
30
|
+
config: Dict[str, Any],
|
|
31
|
+
filters_directory: str = None,
|
|
32
|
+
tests_directory: str = None,
|
|
33
|
+
) -> Result[bool, Exception]:
|
|
34
|
+
"""
|
|
35
|
+
Render the input file using Jinja2 with the provided configuration.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
input_file (Path): The path to the input template file.
|
|
39
|
+
output_file (Path): The path where the rendered output will be saved.
|
|
40
|
+
config (Dict[str, Any]): A dictionary containing the configuration for rendering.
|
|
41
|
+
filters_directory (str, optional): The directory containing custom Jinja2 filters.
|
|
42
|
+
Defaults to None.
|
|
43
|
+
tests_directory (str, optional): The directory containing custom Jinja2 tests.
|
|
44
|
+
Defaults to None.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
Result[bool, Exception]: Success(True) if the template is rendered successfully,
|
|
48
|
+
Failure(exception) if an error occurs during rendering.
|
|
49
|
+
"""
|
|
50
|
+
try:
|
|
51
|
+
jenv = jinja2.Environment(
|
|
52
|
+
loader=jinja2.FileSystemLoader(input_file.parent)
|
|
53
|
+
)
|
|
54
|
+
if filters_directory:
|
|
55
|
+
filters = load_functions_from_directory(filters_directory)
|
|
56
|
+
jenv.filters.update(filters) # Add custom filters functions
|
|
57
|
+
if tests_directory:
|
|
58
|
+
tests = load_functions_from_directory(tests_directory)
|
|
59
|
+
jenv.tests.update(tests) # Add custom tests
|
|
60
|
+
template = jenv.get_template(input_file.name)
|
|
61
|
+
rendered_content = template.render(config)
|
|
62
|
+
|
|
63
|
+
with open(output_file, "w", encoding="utf-8") as f:
|
|
64
|
+
f.write(rendered_content)
|
|
65
|
+
|
|
66
|
+
logger.info(f"Rendered output written to {output_file}")
|
|
67
|
+
return Success(True)
|
|
68
|
+
# pylint: disable=W0718
|
|
69
|
+
except Exception as e:
|
|
70
|
+
logger.error(f"Failed to render template: {e}")
|
|
71
|
+
return Failure(e)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# pylint: disable=R0913,R0917,E1120
|
|
75
|
+
def process_template_with_validation(
|
|
76
|
+
variables: tuple[Path],
|
|
77
|
+
template_filters_directory: Path,
|
|
78
|
+
template_tests_directory: Path,
|
|
79
|
+
output: Path,
|
|
80
|
+
input_file: Path,
|
|
81
|
+
model_file: Path = None,
|
|
82
|
+
class_model: str = None,
|
|
83
|
+
) -> Result[Dict, Exception]:
|
|
84
|
+
"""
|
|
85
|
+
Validates merged configurations against a Pydantic model and renders an input template.
|
|
86
|
+
|
|
87
|
+
Parameters:
|
|
88
|
+
- variables (tuple[Path]): A tuple of file paths containing configuration variables.
|
|
89
|
+
- template_filters_directory (Path): The directory containing custom template filters.
|
|
90
|
+
- template_tests_directory (Path): The directory containing custom template tests.
|
|
91
|
+
- output (Path): The path where the rendered template will be saved.
|
|
92
|
+
- input_file (Path): The path to the input template file.
|
|
93
|
+
- model_file (Path, optional): The path to a Pydantic model file. If provided,
|
|
94
|
+
the configuration will be validated against this model.
|
|
95
|
+
- class_model (str, optional): The name of the model class within the `model_file`.
|
|
96
|
+
Required if `model_file` is provided.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
- Result[Dict, Exception]: A result object containing either the rendered template
|
|
100
|
+
configuration as a dictionary or an exception if any step fails.
|
|
101
|
+
"""
|
|
102
|
+
|
|
103
|
+
merged_config = None
|
|
104
|
+
match load_and_merge_configs(variables):
|
|
105
|
+
case Success(value):
|
|
106
|
+
merged_config = value
|
|
107
|
+
case Failure(value):
|
|
108
|
+
return Failure(
|
|
109
|
+
ValueError(f"Failed to load configs from files: {value}")
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
logger.debug(f"merged_config: {merged_config}")
|
|
113
|
+
env_config = resolve_env_variables(merged_config)
|
|
114
|
+
logger.debug(f"env_config: {env_config}")
|
|
115
|
+
filters_path = template_filters_directory
|
|
116
|
+
tests_path = template_tests_directory
|
|
117
|
+
logger.debug(f"filters_path: {filters_path}")
|
|
118
|
+
template_config = render_templates_with_filters(
|
|
119
|
+
env_config, str(filters_path), str(tests_path)
|
|
120
|
+
)
|
|
121
|
+
logger.info(json.dumps(template_config))
|
|
122
|
+
|
|
123
|
+
# Load the model class
|
|
124
|
+
if model_file and class_model:
|
|
125
|
+
model_class = load_model_class(model_file, class_model)
|
|
126
|
+
if not model_class:
|
|
127
|
+
return Failure(
|
|
128
|
+
ValueError("Failed to load the specified model class.")
|
|
129
|
+
)
|
|
130
|
+
# Validate the merged configuration
|
|
131
|
+
validation_result = validate_config(template_config, model_class)
|
|
132
|
+
if isinstance(validation_result, Failure):
|
|
133
|
+
return Failure(
|
|
134
|
+
ValueError(f"Given config is invalid {validation_result}")
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
match render_jinja_template(
|
|
138
|
+
input_file,
|
|
139
|
+
output,
|
|
140
|
+
template_config,
|
|
141
|
+
template_filters_directory,
|
|
142
|
+
template_tests_directory,
|
|
143
|
+
):
|
|
144
|
+
case Failure(value):
|
|
145
|
+
return Failure(ValueError(f"Failed to render template {value}"))
|
|
146
|
+
|
|
147
|
+
return Success(template_config)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# pylint: disable=R0913,R0917,E1120
|
|
151
|
+
@click.command()
|
|
152
|
+
@click.option(
|
|
153
|
+
"-v",
|
|
154
|
+
"--variables",
|
|
155
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
156
|
+
multiple=True,
|
|
157
|
+
required=True,
|
|
158
|
+
help="Path(s) to the various configuration files.",
|
|
159
|
+
)
|
|
160
|
+
@click.option(
|
|
161
|
+
"-m",
|
|
162
|
+
"--model-file",
|
|
163
|
+
type=click.Path(
|
|
164
|
+
exists=True, file_okay=True, dir_okay=False, path_type=Path
|
|
165
|
+
),
|
|
166
|
+
required=False,
|
|
167
|
+
default=None,
|
|
168
|
+
help="Path to the Python file containing the Pydantic model class.",
|
|
169
|
+
)
|
|
170
|
+
@click.option(
|
|
171
|
+
"-c",
|
|
172
|
+
"--class-model",
|
|
173
|
+
type=str,
|
|
174
|
+
required=False,
|
|
175
|
+
default=None,
|
|
176
|
+
help="Name of the Pydantic model class to validate against.",
|
|
177
|
+
)
|
|
178
|
+
@click.option(
|
|
179
|
+
"-f",
|
|
180
|
+
"--template-filters-directory",
|
|
181
|
+
type=click.Path(
|
|
182
|
+
exists=True, file_okay=False, dir_okay=True, path_type=Path
|
|
183
|
+
),
|
|
184
|
+
default=None,
|
|
185
|
+
help="Directory containing custom Jinja2 filter functions.",
|
|
186
|
+
)
|
|
187
|
+
@click.option(
|
|
188
|
+
"-t",
|
|
189
|
+
"--template-tests-directory",
|
|
190
|
+
type=click.Path(
|
|
191
|
+
exists=True, file_okay=False, dir_okay=True, path_type=Path
|
|
192
|
+
),
|
|
193
|
+
default=None,
|
|
194
|
+
help="Directory containing custom Jinja2 test functions.",
|
|
195
|
+
)
|
|
196
|
+
@click.option(
|
|
197
|
+
"-o",
|
|
198
|
+
"--output",
|
|
199
|
+
type=click.Path(dir_okay=False, writable=True, path_type=Path),
|
|
200
|
+
required=True,
|
|
201
|
+
help="Path to the output file where the rendered content will be written.",
|
|
202
|
+
)
|
|
203
|
+
@click.argument(
|
|
204
|
+
"input_file",
|
|
205
|
+
type=click.Path(exists=True, dir_okay=False, path_type=Path),
|
|
206
|
+
required=True,
|
|
207
|
+
# help="Path to the input template file.",
|
|
208
|
+
)
|
|
209
|
+
def main(
|
|
210
|
+
variables: tuple[Path],
|
|
211
|
+
model_file: Path,
|
|
212
|
+
class_model: str,
|
|
213
|
+
template_filters_directory: Path,
|
|
214
|
+
template_tests_directory: Path,
|
|
215
|
+
output: Path,
|
|
216
|
+
input_file: Path,
|
|
217
|
+
):
|
|
218
|
+
"""
|
|
219
|
+
Validate merged configurations against a Pydantic model and render an input template.
|
|
220
|
+
"""
|
|
221
|
+
match process_template_with_validation(
|
|
222
|
+
variables,
|
|
223
|
+
template_filters_directory,
|
|
224
|
+
template_tests_directory,
|
|
225
|
+
output,
|
|
226
|
+
input_file,
|
|
227
|
+
model_file,
|
|
228
|
+
class_model,
|
|
229
|
+
):
|
|
230
|
+
case Success(value):
|
|
231
|
+
logger.info("Command run successfully")
|
|
232
|
+
case Failure(value):
|
|
233
|
+
logger.error(f"Command failed with error {value}")
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
if __name__ == "__main__":
|
|
237
|
+
main()
|
masha/config_loader.py
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Module Description:
|
|
5
|
+
This module provides functionality to load and merge configuration files from various
|
|
6
|
+
formats (YAML, JSON, TOML, Properties) into a single dictionary. It also includes a
|
|
7
|
+
command-line interface (CLI) entry point to facilitate loading and merging configurations.
|
|
8
|
+
|
|
9
|
+
Functions:
|
|
10
|
+
- `load_config(file_path: Path) -> dict`: Loads a configuration file into a dictionary
|
|
11
|
+
based on its file extension.
|
|
12
|
+
- `merge_configs(configs: Dict[str, Any]) -> dict`: Merges multiple dictionaries into one.
|
|
13
|
+
If there are overlapping keys, the values from later dictionaries will overwrite those from
|
|
14
|
+
earlier ones.
|
|
15
|
+
- `load_and_merge_configs(config_paths: list[Path])`: Loads and merges multiple configuration
|
|
16
|
+
files specified by their paths.
|
|
17
|
+
|
|
18
|
+
CLI Entry Point:
|
|
19
|
+
- `main()`: The main function that serves as the entry point for the command-line interface.
|
|
20
|
+
It parses command-line arguments, loads and merges configurations, and prints the merged
|
|
21
|
+
configuration in JSON format.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import configparser
|
|
26
|
+
import json
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any, Dict
|
|
29
|
+
|
|
30
|
+
import toml
|
|
31
|
+
import yaml
|
|
32
|
+
from returns.result import Failure, Result, Success
|
|
33
|
+
|
|
34
|
+
# pylint: disable=W1203
|
|
35
|
+
from masha.logger_factory import create_logger
|
|
36
|
+
|
|
37
|
+
logger = create_logger("masha")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Function to load configuration files
|
|
41
|
+
def load_config(file_path: Path) -> Result[{}, dict]:
|
|
42
|
+
"""
|
|
43
|
+
Load configuration from a file based on its extension.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
file_path (Path): The path to the configuration file.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Result: A dictionary containing the configuration data if successful,
|
|
50
|
+
or an error message if the file type is unsupported.
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
if file_path.suffix in {".yaml", ".yml"}:
|
|
54
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
55
|
+
return Success(yaml.safe_load(f))
|
|
56
|
+
elif file_path.suffix == ".json":
|
|
57
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
58
|
+
return Success(json.load(f))
|
|
59
|
+
elif file_path.suffix == ".toml":
|
|
60
|
+
with open(file_path, "r", encoding="utf-8") as f:
|
|
61
|
+
return Success(toml.load(f))
|
|
62
|
+
elif file_path.suffix == ".properties":
|
|
63
|
+
config = configparser.ConfigParser()
|
|
64
|
+
config.read(file_path)
|
|
65
|
+
return Success(
|
|
66
|
+
{
|
|
67
|
+
section: dict(config[section])
|
|
68
|
+
for section in config.sections()
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
else:
|
|
72
|
+
return Failure(
|
|
73
|
+
{"error": f"Unsupported file type: {file_path.suffix}"}
|
|
74
|
+
)
|
|
75
|
+
except FileNotFoundError as e:
|
|
76
|
+
return Failure({"error": f"File not found: {e}"})
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# Function to merge multiple dictionaries
|
|
80
|
+
def merge_configs(configs: Dict[str, Any]) -> dict:
|
|
81
|
+
"""
|
|
82
|
+
Merge multiple dictionaries into one.
|
|
83
|
+
|
|
84
|
+
Parameters:
|
|
85
|
+
configs (Dict[str, Any]): A dictionary where each key is a string representing a
|
|
86
|
+
configuration name, and the value is another dictionary
|
|
87
|
+
containing the configuration settings.
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
dict: A single dictionary that contains all the configurations from the input
|
|
91
|
+
dictionaries. If there are overlapping keys, the values from later dictionaries
|
|
92
|
+
will overwrite those from earlier ones.
|
|
93
|
+
"""
|
|
94
|
+
merged_config = {}
|
|
95
|
+
for config in configs:
|
|
96
|
+
merged_config.update(config)
|
|
97
|
+
logger.debug(f"merged_config = {merged_config}")
|
|
98
|
+
return merged_config
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def load_and_merge_configs(config_paths: list[Path]) -> Result[Dict, Dict]:
|
|
102
|
+
"""
|
|
103
|
+
Load and merge multiple configuration files.
|
|
104
|
+
|
|
105
|
+
This function takes a list of file paths to configuration files, loads each one,
|
|
106
|
+
and merges them into a single dictionary. If any file fails to load or merge,
|
|
107
|
+
the function returns an error message.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
config_paths (list[Path]): A list of file paths to the configuration files.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Result[dict, str]: A `Success` containing the merged configuration dictionary
|
|
114
|
+
if all files are processed successfully.
|
|
115
|
+
Otherwise, a `Failure` containing an error message indicating
|
|
116
|
+
which file caused the issue.
|
|
117
|
+
|
|
118
|
+
Raises:
|
|
119
|
+
ValueError: If any of the provided file paths are not valid or do not exist.
|
|
120
|
+
"""
|
|
121
|
+
configs = []
|
|
122
|
+
for config_path in config_paths:
|
|
123
|
+
logger.debug(f"Loading file: {config_path}")
|
|
124
|
+
match load_config(config_path):
|
|
125
|
+
case Success(config_data):
|
|
126
|
+
configs.append(config_data)
|
|
127
|
+
case Failure(value):
|
|
128
|
+
msg = f"Error processing file {config_path}: {value}"
|
|
129
|
+
logger.warning(msg)
|
|
130
|
+
return Failure({"error": msg})
|
|
131
|
+
merged_config = merge_configs(configs)
|
|
132
|
+
return Success(merged_config)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# CLI entry point
|
|
136
|
+
def main():
|
|
137
|
+
"""
|
|
138
|
+
Validates merged configuration files against a Pydantic model.
|
|
139
|
+
|
|
140
|
+
This function sets up an argument parser to accept paths to configuration files.
|
|
141
|
+
It then loads and merges these configuration files, printing the merged result in JSON format.
|
|
142
|
+
"""
|
|
143
|
+
parser = argparse.ArgumentParser(
|
|
144
|
+
description="Validate merged configuration files against a Pydantic model."
|
|
145
|
+
)
|
|
146
|
+
parser.add_argument(
|
|
147
|
+
"-v",
|
|
148
|
+
"--variables",
|
|
149
|
+
nargs="+",
|
|
150
|
+
type=Path,
|
|
151
|
+
required=True,
|
|
152
|
+
help="Paths to the configuration files.",
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
args = parser.parse_args()
|
|
156
|
+
|
|
157
|
+
# # Load the model class
|
|
158
|
+
# model_class = load_model_class(args.model_file, args.class_model)
|
|
159
|
+
# if not model_class:
|
|
160
|
+
# return
|
|
161
|
+
|
|
162
|
+
# Load and merge all configuration files
|
|
163
|
+
merged_config = None
|
|
164
|
+
match load_and_merge_configs(args.variables):
|
|
165
|
+
case Success(val):
|
|
166
|
+
merged_config = val
|
|
167
|
+
case Failure(val):
|
|
168
|
+
logger.warning(f"Failed to load config: {val}")
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
print(json.dumps(merged_config))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
if __name__ == "__main__":
|
|
175
|
+
main()
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Validate the configuration against pydantic Model class
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ValidationError
|
|
10
|
+
from returns.result import Failure, Result, Success
|
|
11
|
+
|
|
12
|
+
# pylint: disable=W1203
|
|
13
|
+
from masha.config_loader import load_and_merge_configs
|
|
14
|
+
from masha.env_loader import resolve_env_variables
|
|
15
|
+
from masha.logger_factory import create_logger
|
|
16
|
+
from masha.template_renderer import render_templates_with_filters
|
|
17
|
+
|
|
18
|
+
logger = create_logger("masha")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# Main validation function
|
|
22
|
+
def validate_config(
|
|
23
|
+
config_data: dict, model_class: BaseModel
|
|
24
|
+
) -> Result[str, str]:
|
|
25
|
+
"""
|
|
26
|
+
Validate the configuration data against the provided Pydantic model class.
|
|
27
|
+
|
|
28
|
+
Parameters:
|
|
29
|
+
config_data (dict): A dictionary containing the configuration data to be validated.
|
|
30
|
+
model_class (BaseModel): The Pydantic model class that defines the expected structure of
|
|
31
|
+
the configuration data.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
None
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
ValidationError: If the configuration data does not match the expected structure defined
|
|
38
|
+
by `model_class`.
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
model_instance = model_class(**config_data)
|
|
42
|
+
msg = f"Validation successful: {model_instance}"
|
|
43
|
+
logger.debug(msg)
|
|
44
|
+
return Success(msg)
|
|
45
|
+
except ValidationError as e:
|
|
46
|
+
msg = f"Validation failed with errors: {e}"
|
|
47
|
+
logger.warning(msg)
|
|
48
|
+
return Failure(msg)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# pylint: disable=W0122,W0718
|
|
52
|
+
def load_model_class(model_file_path: Path, model_class_name: str):
|
|
53
|
+
"""
|
|
54
|
+
Load a model class from a specified file path.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
model_file_path (Path): The path to the file containing the model class.
|
|
58
|
+
model_class_name (str): The name of the model class to load.
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Optional[Type]: The loaded model class if successful, otherwise None.
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
TypeError: If the specified class is not a subclass of Pydantic BaseModel.
|
|
65
|
+
|
|
66
|
+
Notes:
|
|
67
|
+
- This function reads the content of the file at `model_file_path` and executes it in a
|
|
68
|
+
local namespace.
|
|
69
|
+
- It then attempts to retrieve the class named `model_class_name` from this namespace.
|
|
70
|
+
- If the retrieved class is not a subclass of `BaseModel`, a `TypeError` is raised.
|
|
71
|
+
- Any exceptions encountered during the execution or retrieval process are logged as
|
|
72
|
+
warnings.
|
|
73
|
+
|
|
74
|
+
Example:
|
|
75
|
+
>>> model_file_path = Path("path/to/model.py")
|
|
76
|
+
>>> model_class_name = "MyModel"
|
|
77
|
+
>>> MyModelClass = load_model_class(model_file_path, model_class_name)
|
|
78
|
+
>>> if MyModelClass is not None:
|
|
79
|
+
... print(f"Model class {model_class_name} loaded successfully.")
|
|
80
|
+
"""
|
|
81
|
+
try:
|
|
82
|
+
model_globals = {}
|
|
83
|
+
exec(model_file_path.read_text(), model_globals)
|
|
84
|
+
model_class = model_globals[model_class_name]
|
|
85
|
+
if not issubclass(model_class, BaseModel):
|
|
86
|
+
raise TypeError(
|
|
87
|
+
f"{model_class_name} is not a subclass of Pydantic BaseModel."
|
|
88
|
+
)
|
|
89
|
+
return model_class
|
|
90
|
+
except Exception as e:
|
|
91
|
+
logger.warning(f"Failed to load the model class: {e}")
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# CLI entry point
|
|
96
|
+
def main():
|
|
97
|
+
"""
|
|
98
|
+
test config validation
|
|
99
|
+
"""
|
|
100
|
+
parser = argparse.ArgumentParser(
|
|
101
|
+
description="Validate merged configurations against a Pydantic model."
|
|
102
|
+
)
|
|
103
|
+
parser.add_argument(
|
|
104
|
+
"-v",
|
|
105
|
+
"--variables",
|
|
106
|
+
type=Path,
|
|
107
|
+
nargs="+",
|
|
108
|
+
required=True,
|
|
109
|
+
help="Paths to the various configuration files.",
|
|
110
|
+
)
|
|
111
|
+
parser.add_argument(
|
|
112
|
+
"-m",
|
|
113
|
+
"--model-file",
|
|
114
|
+
type=Path,
|
|
115
|
+
required=True,
|
|
116
|
+
help="Path to the Python file containing the Pydantic model class.",
|
|
117
|
+
)
|
|
118
|
+
parser.add_argument(
|
|
119
|
+
"-c",
|
|
120
|
+
"--class-model",
|
|
121
|
+
type=str,
|
|
122
|
+
required=True,
|
|
123
|
+
help="Name of the Pydantic model class to validate against.",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
args = parser.parse_args()
|
|
127
|
+
|
|
128
|
+
# Load the model class
|
|
129
|
+
model_class = load_model_class(args.model_file, args.class_model)
|
|
130
|
+
if not model_class:
|
|
131
|
+
return
|
|
132
|
+
|
|
133
|
+
# Load and merge all configuration files
|
|
134
|
+
merged_config = None
|
|
135
|
+
match load_and_merge_configs(args.variables):
|
|
136
|
+
case Success(value):
|
|
137
|
+
merged_config = value
|
|
138
|
+
case Failure(value):
|
|
139
|
+
logger.warning(f"Failed to load configs: {value}")
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
logger.info(merged_config)
|
|
143
|
+
env_config = resolve_env_variables(merged_config)
|
|
144
|
+
logger.info(env_config)
|
|
145
|
+
filters_path = Path(__file__).parent / "filters"
|
|
146
|
+
logger.debug(filters_path)
|
|
147
|
+
temp_config = render_templates_with_filters(env_config, str(filters_path))
|
|
148
|
+
logger.info(temp_config)
|
|
149
|
+
|
|
150
|
+
# Validate the merged configuration
|
|
151
|
+
# validate_merged_config(env_config, model_class)
|
|
152
|
+
validation_result = validate_config(temp_config, model_class)
|
|
153
|
+
if isinstance(validation_result, Success):
|
|
154
|
+
logger.info(f"Given config is valid {validation_result}")
|
|
155
|
+
else:
|
|
156
|
+
logger.warning(f"Given config is invalid {validation_result}")
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
# masha/config_validator.py -v test/config-b.yaml -m test/model.py -c ConfigModel
|
|
161
|
+
main()
|
masha/env_loader.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Loads the environment variable in configuration value
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from returns.result import Failure, Success
|
|
13
|
+
|
|
14
|
+
# pylint: disable=W1203
|
|
15
|
+
from masha.config_loader import load_and_merge_configs
|
|
16
|
+
from masha.logger_factory import create_logger
|
|
17
|
+
|
|
18
|
+
logger = create_logger("masha")
|
|
19
|
+
|
|
20
|
+
_path_matcher = re.compile(
|
|
21
|
+
r"\$\{(?P<env_name>[^}^{:]+)(?::(?P<default_value>[^}^{]*))?\}"
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def resolve_env_variables(config) -> dict:
|
|
26
|
+
"""
|
|
27
|
+
Resolve environment variables in a configuration dictionary.
|
|
28
|
+
|
|
29
|
+
This function recursively searches for environment variable placeholders in
|
|
30
|
+
the given configuration. Placeholders are in the format ${ENV_VAR: default_value},
|
|
31
|
+
where ENV_VAR is the name of the environment variable and default_value is an
|
|
32
|
+
optional default value if the environment variable is not set.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
config (dict): The configuration dictionary containing potential environment
|
|
36
|
+
variable placeholders.
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
dict: A new dictionary with all environment variable placeholders resolved.
|
|
40
|
+
"""
|
|
41
|
+
pattern = re.compile(
|
|
42
|
+
r"\$\{(\w+):\s*(.*?)\}"
|
|
43
|
+
) # Match ${ENV_VAR: default_value}
|
|
44
|
+
|
|
45
|
+
def resolve_value(value):
|
|
46
|
+
if isinstance(value, str):
|
|
47
|
+
match = pattern.fullmatch(value)
|
|
48
|
+
if match:
|
|
49
|
+
env_var, default_value = match.groups()
|
|
50
|
+
if default_value == "null":
|
|
51
|
+
default_value = None
|
|
52
|
+
return os.getenv(env_var, default_value)
|
|
53
|
+
elif isinstance(
|
|
54
|
+
value, dict
|
|
55
|
+
): # Recursively resolve nested dictionaries
|
|
56
|
+
return {k: resolve_value(v) for k, v in value.items()}
|
|
57
|
+
elif isinstance(value, list): # Recursively resolve lists
|
|
58
|
+
return [resolve_value(v) for v in value]
|
|
59
|
+
return value # Return unchanged if no match
|
|
60
|
+
|
|
61
|
+
return {key: resolve_value(value) for key, value in config.items()}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main():
|
|
65
|
+
"""
|
|
66
|
+
Validates merged configuration files against a Pydantic model.
|
|
67
|
+
|
|
68
|
+
This function sets up an argument parser to accept paths to configuration files.
|
|
69
|
+
It then loads and merges these configuration files, printing the merged result in JSON format.
|
|
70
|
+
"""
|
|
71
|
+
conf_file = Path(__file__).parent.parent / "test" / "env_config.yaml"
|
|
72
|
+
config = None
|
|
73
|
+
match load_and_merge_configs([conf_file]):
|
|
74
|
+
case Success(value):
|
|
75
|
+
config = value
|
|
76
|
+
case Failure(value):
|
|
77
|
+
logger.warning(f"Failed to read configs: {value}")
|
|
78
|
+
return
|
|
79
|
+
logger.debug(f"config = {config}")
|
|
80
|
+
os.environ["ENV_B"] = "default_not_used_b"
|
|
81
|
+
env_config = resolve_env_variables(config)
|
|
82
|
+
logger.debug(f"env_config = {json.dumps(env_config)}")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
if __name__ == "__main__":
|
|
86
|
+
main()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides utility functions for string manipulation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def uppercase(s: str) -> str:
|
|
7
|
+
"""
|
|
8
|
+
Converts the given string to uppercase.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
s (str): The input string.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
str: The uppercase version of the input string.
|
|
15
|
+
"""
|
|
16
|
+
return s.upper()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def lowercase(s: str) -> str:
|
|
20
|
+
"""
|
|
21
|
+
Converts the given string to lowercase.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
s (str): The input string.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
str: The lowercase version of the input string.
|
|
28
|
+
"""
|
|
29
|
+
return s.lower()
|
masha/logger_factory.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module for creating and configuring logger instances.
|
|
3
|
+
|
|
4
|
+
This module provides a function to create a logger instance configured from a
|
|
5
|
+
logging configuration file. The logger is set up using the settings specified
|
|
6
|
+
in 'logging.conf'. If the configuration file is not found, a FileNotFoundError
|
|
7
|
+
is raised.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import logging.config
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def create_logger(name):
|
|
16
|
+
"""
|
|
17
|
+
Creates and returns a logger instance configured from a logging configuration file.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
name (str): The name of the logger to be created.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
logging.Logger: A logger instance configured according to the settings
|
|
24
|
+
in 'logging.conf'.
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
FileNotFoundError: If the 'logging.conf' file is not found in the same
|
|
28
|
+
directory as this script.
|
|
29
|
+
"""
|
|
30
|
+
log_conf_file = Path(__file__).parent / "logging.conf"
|
|
31
|
+
|
|
32
|
+
if not log_conf_file.exists():
|
|
33
|
+
raise FileNotFoundError(
|
|
34
|
+
f"The logging configuration file '{log_conf_file}' was not found."
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
logging.config.fileConfig(log_conf_file)
|
|
38
|
+
return logging.getLogger(name)
|
masha/logging.conf
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[loggers]
|
|
2
|
+
keys=root,masha
|
|
3
|
+
|
|
4
|
+
[handlers]
|
|
5
|
+
keys=consoleHandler
|
|
6
|
+
|
|
7
|
+
[formatters]
|
|
8
|
+
keys=simpleFormatter
|
|
9
|
+
|
|
10
|
+
[logger_root]
|
|
11
|
+
level=DEBUG
|
|
12
|
+
handlers=consoleHandler
|
|
13
|
+
|
|
14
|
+
[logger_masha]
|
|
15
|
+
level=INFO
|
|
16
|
+
handlers=consoleHandler
|
|
17
|
+
qualname=masha
|
|
18
|
+
propagate=0
|
|
19
|
+
|
|
20
|
+
[handler_consoleHandler]
|
|
21
|
+
class=StreamHandler
|
|
22
|
+
level=DEBUG
|
|
23
|
+
formatter=simpleFormatter
|
|
24
|
+
args=(sys.stdout,)
|
|
25
|
+
|
|
26
|
+
[formatter_simpleFormatter]
|
|
27
|
+
#format=%(asctime)s %(levelname)s %(name)s %(message)s
|
|
28
|
+
format=%(asctime)s %(levelname)-7s [%(filename)-20s:%(lineno)s:%(funcName)22s] %(message)s
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Render jinja2 template defined in configuration
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import importlib.util
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import jinja2
|
|
12
|
+
|
|
13
|
+
# pylint: disable=W1203
|
|
14
|
+
from masha.logger_factory import create_logger
|
|
15
|
+
|
|
16
|
+
logger = create_logger("masha")
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def load_functions_from_file(file: str):
|
|
20
|
+
"""Loads all Python functions from a given file.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
file (str): The path to the Python file from which to load functions.
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
dict: A dictionary containing function names as keys and their corresponding
|
|
27
|
+
callable objects as values.
|
|
28
|
+
"""
|
|
29
|
+
functions = {}
|
|
30
|
+
if os.path.exists(file) and file.endswith(".py"):
|
|
31
|
+
module_name = file[:-3] # Remove '.py' extension
|
|
32
|
+
spec = importlib.util.spec_from_file_location(module_name, file)
|
|
33
|
+
module = importlib.util.module_from_spec(spec)
|
|
34
|
+
spec.loader.exec_module(module)
|
|
35
|
+
|
|
36
|
+
for attr_name in dir(module):
|
|
37
|
+
attr = getattr(module, attr_name)
|
|
38
|
+
if callable(attr) and not attr_name.startswith(
|
|
39
|
+
"_"
|
|
40
|
+
): # Only include functions
|
|
41
|
+
functions[attr_name] = attr
|
|
42
|
+
return functions
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_functions_from_directory(directory: str):
|
|
46
|
+
"""Loads all Python functions from files in the given directory as Jinja2 filters.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
directory (str): The path to the directory containing Python files with filter functions.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
dict: A dictionary containing filter names as keys and their corresponding callable
|
|
53
|
+
objects as values.
|
|
54
|
+
"""
|
|
55
|
+
fxns = {}
|
|
56
|
+
if os.path.exists(directory):
|
|
57
|
+
for filename in os.listdir(directory):
|
|
58
|
+
file_path = os.path.join(directory, filename)
|
|
59
|
+
fxns.update(load_functions_from_file(file_path))
|
|
60
|
+
return fxns
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def render_templates_with_filters(
|
|
64
|
+
input_dict: dict,
|
|
65
|
+
filters_directory: str = None,
|
|
66
|
+
tests_directory: str = None,
|
|
67
|
+
max_iterations=10,
|
|
68
|
+
) -> dict:
|
|
69
|
+
"""
|
|
70
|
+
Renders templates in a dictionary using Jinja2, applying custom filters and tests.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
input_dict (dict): The dictionary containing the template strings to be rendered.
|
|
74
|
+
filters_directory (str, optional): Path to the directory containing custom filters.
|
|
75
|
+
Defaults to None.
|
|
76
|
+
tests_directory (str, optional): Path to the directory containing custom tests.
|
|
77
|
+
Defaults to None.
|
|
78
|
+
max_iterations (int, optional): Maximum number of iterations for rendering. Defaults to 10.
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
dict: The dictionary with rendered template strings.
|
|
82
|
+
"""
|
|
83
|
+
env = jinja2.Environment()
|
|
84
|
+
if filters_directory:
|
|
85
|
+
filters = load_functions_from_directory(filters_directory)
|
|
86
|
+
env.filters.update(filters) # Add custom filters
|
|
87
|
+
if tests_directory:
|
|
88
|
+
tests = load_functions_from_directory(tests_directory)
|
|
89
|
+
env.tests.update(tests)
|
|
90
|
+
|
|
91
|
+
rendered_dict = input_dict.copy()
|
|
92
|
+
|
|
93
|
+
def recursive_render(value):
|
|
94
|
+
if isinstance(value, dict):
|
|
95
|
+
return {k: recursive_render(v) for k, v in value.items()}
|
|
96
|
+
if isinstance(value, str):
|
|
97
|
+
template = env.from_string(value)
|
|
98
|
+
return template.render(rendered_dict)
|
|
99
|
+
return value
|
|
100
|
+
|
|
101
|
+
for _ in range(max_iterations):
|
|
102
|
+
new_dict = recursive_render(rendered_dict)
|
|
103
|
+
if new_dict == rendered_dict:
|
|
104
|
+
break
|
|
105
|
+
rendered_dict = new_dict
|
|
106
|
+
return rendered_dict
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def main():
|
|
110
|
+
"""main function to test this module"""
|
|
111
|
+
inp = {
|
|
112
|
+
"c": "from {{ b }}",
|
|
113
|
+
"a": "val_a",
|
|
114
|
+
"b": "from_{{ a | uppercase }}",
|
|
115
|
+
"d": {"e": "{{a}}"},
|
|
116
|
+
}
|
|
117
|
+
# inp = {"name": "test", "version": "0.0.2", "debug": "false", "age": 14}
|
|
118
|
+
logger.debug(f"imput = {inp}")
|
|
119
|
+
filters_path = Path(__file__).parent / "filters"
|
|
120
|
+
logger.debug(f"filters_path = {filters_path}")
|
|
121
|
+
# Path(__file__).parent / "tests"
|
|
122
|
+
rendered = render_templates_with_filters(inp, str(filters_path))
|
|
123
|
+
logger.info(f"rendered = {rendered}")
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
main()
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module provides utility functions for basic mathematical operations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def is_even(number):
|
|
7
|
+
"""
|
|
8
|
+
Check if a given number is even.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
number (int): The number to check.
|
|
12
|
+
|
|
13
|
+
Returns:
|
|
14
|
+
bool: True if the number is even, False otherwise.
|
|
15
|
+
"""
|
|
16
|
+
return number % 2 == 0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_is_even(number):
|
|
20
|
+
"""
|
|
21
|
+
Check if a given number is even for yasha.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
number (int): The number to check for evne.
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
bool: True if the number is even , False otherwise.
|
|
28
|
+
"""
|
|
29
|
+
return number % 2 == 0
|
masha/version.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [2025] [Mitesh Singh Jat]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: masha
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: MASHup of Configuration Loading from several file types and run yAsha like Jinja2 template rendition with Validation
|
|
5
|
+
License: MIT
|
|
6
|
+
Author: Mitesh Jat
|
|
7
|
+
Author-email: mitesh.singh.jat@gmail.com
|
|
8
|
+
Requires-Python: >=3.13,<4.0
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
12
|
+
Requires-Dist: Jinja2 (>=3.1.5,<4.0.0)
|
|
13
|
+
Requires-Dist: click (>=8.1.8,<9.0.0)
|
|
14
|
+
Requires-Dist: pydantic (>=2.10.6,<3.0.0)
|
|
15
|
+
Requires-Dist: pyyaml (==6.0.2)
|
|
16
|
+
Requires-Dist: result (>=0.17.0,<0.18.0)
|
|
17
|
+
Requires-Dist: returns (>=0.24.0,<0.25.0)
|
|
18
|
+
Requires-Dist: toml (==0.10.2)
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# MASHA
|
|
22
|
+
|
|
23
|
+
MASHup of Configuration Loading from several file types and run [yAsha](https://github.com/kblomqvist/yasha/tree/master/yasha) like Jinja2 template rendition with [Validation](https://github.com/miteshbsjat/cli_config_validator).
|
|
24
|
+
|
|
25
|
+
## Motivation
|
|
26
|
+
* `MASHA` or `Magnificient yASHA`, is the name chosen, to show the amalgamation(`MASHa UP`) of my ideas and [`yasha` tool](https://github.com/kblomqvist/yasha). It has been inspired by these following magnificient tools or libraries:-
|
|
27
|
+
* [`env-yaml-python`](https://github.com/iamKunal/env-yaml-python) helps in using ENV variable in the yaml configuration file.
|
|
28
|
+
* [`j2yaml`](https://pypi.org/project/j2yaml/) looks good tool to be used for having templates inside `yaml` configuration files.
|
|
29
|
+
* [`cli_config_validator`](https://github.com/miteshbsjat/cli_config_validator) configuration validation.
|
|
30
|
+
* [`yasha`](https://github.com/kblomqvist/yasha) code generation tool based on jinja2 template rendition.
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
<table>
|
|
34
|
+
<tr>
|
|
35
|
+
<td><img src="docs/images/masha.jpg" width="135" height="100"></td>
|
|
36
|
+
<td>MASHA (name) is inspired from Cartoon Series <a href="https://en.wikipedia.org/wiki/Masha_and_the_Bear">Masha and Bear</a>. Like in this cartoon, a girl named `Masha` plays with the bear name `Mishka`, similarly, my daughter named `Diyanjali` plays with her father named `Mitesh` (i.e. me). :) <br />
|
|
37
|
+
The Masha logo is combination of Masha (the girl) and Yasha (the Katana Sword).
|
|
38
|
+
</td>
|
|
39
|
+
</tr>
|
|
40
|
+
</table>
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
You can install `masha` via pip. Ensure you have Python 3.10 or later installed on your system:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
pip install masha
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Alternatively, if you prefer to install from the source code (requires `poetry`), follow these steps:
|
|
51
|
+
|
|
52
|
+
1. Clone the repository:
|
|
53
|
+
```bash
|
|
54
|
+
git clone https://github.com/miteshbsjat/masha.git
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
2. Navigate into the cloned directory:
|
|
58
|
+
```bash
|
|
59
|
+
cd masha
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
3. Install the package using setuptools:
|
|
63
|
+
```bash
|
|
64
|
+
poetry shell
|
|
65
|
+
poetry install
|
|
66
|
+
poetry build
|
|
67
|
+
pip3 install --force-reinstall dist/masha-0.0.0-py3-none-any.whl --user
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
### masha help
|
|
73
|
+
```sh
|
|
74
|
+
$ masha --help
|
|
75
|
+
Usage: masha [OPTIONS] INPUT_FILE
|
|
76
|
+
|
|
77
|
+
Validate merged configurations against a Pydantic model and render an input
|
|
78
|
+
template.
|
|
79
|
+
|
|
80
|
+
Options:
|
|
81
|
+
-v, --variables FILE Path(s) to the various configuration files.
|
|
82
|
+
[required]
|
|
83
|
+
-m, --model-file FILE Path to the Python file containing the
|
|
84
|
+
Pydantic model class.
|
|
85
|
+
-c, --class-model TEXT Name of the Pydantic model class to validate
|
|
86
|
+
against.
|
|
87
|
+
-f, --template-filters-directory DIRECTORY
|
|
88
|
+
Directory containing custom Jinja2 filter
|
|
89
|
+
functions.
|
|
90
|
+
-t, --template-tests-directory DIRECTORY
|
|
91
|
+
Directory containing custom Jinja2 test
|
|
92
|
+
functions.
|
|
93
|
+
-o, --output FILE Path to the output file where the rendered
|
|
94
|
+
content will be written. [required]
|
|
95
|
+
--help Show this message and exit.
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Basic Usage
|
|
99
|
+
|
|
100
|
+
To use `masha`, you can run it from the command line with various options to load configuration files and render templates.
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
masha -v test/config-a.yaml -v test/config-b.yaml \
|
|
104
|
+
-m test/model.py -c ConfigModel \
|
|
105
|
+
-f masha/filters -t masha/tests \
|
|
106
|
+
-o /tmp/demo.txt \
|
|
107
|
+
test/input.txt.j2
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Advanced Usage
|
|
111
|
+
|
|
112
|
+
#### Specifying Multiple Configurations
|
|
113
|
+
|
|
114
|
+
You can load multiple configuration files which will be merged together:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
masha -v config1.yaml -v config2.json -o result.txt advanced_template.j2
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### Using Environment Variables
|
|
121
|
+
|
|
122
|
+
`masha` also supports environment variables to override or extend configurations:
|
|
123
|
+
in `env_example.j2` file `This came from env MY_VAR = ${MY_VAR:some_default_value}`
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
export MY_VAR="some_value"
|
|
127
|
+
masha -v default_config.yaml --output env_output.txt env_example.j2
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
output
|
|
131
|
+
```
|
|
132
|
+
This came from env MY_VAR = some_value
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
### Example Configuration File (`config.yaml`)
|
|
137
|
+
|
|
138
|
+
Here is an example configuration file in YAML format:
|
|
139
|
+
|
|
140
|
+
```yaml
|
|
141
|
+
app:
|
|
142
|
+
name: MyApplication
|
|
143
|
+
version: 1.0.0
|
|
144
|
+
|
|
145
|
+
database:
|
|
146
|
+
host: localhost
|
|
147
|
+
port: 5432
|
|
148
|
+
username: user
|
|
149
|
+
password: pass
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Example Template File (`template.j2`)
|
|
153
|
+
|
|
154
|
+
Here is a simple Jinja2 template file:
|
|
155
|
+
|
|
156
|
+
```jinja
|
|
157
|
+
Welcome to {{ app.name }} version {{ app.version }}!
|
|
158
|
+
|
|
159
|
+
Database configuration:
|
|
160
|
+
- Host: {{ database.host }}
|
|
161
|
+
- Port: {{ database.port }}
|
|
162
|
+
- Username: {{ database.username }}
|
|
163
|
+
- Password: {{ database.password }}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### Output
|
|
167
|
+
|
|
168
|
+
Running the command with the above example files would produce an output file (`output.txt`) like this:
|
|
169
|
+
|
|
170
|
+
```
|
|
171
|
+
Welcome to MyApplication version 1.0.0!
|
|
172
|
+
|
|
173
|
+
Database configuration:
|
|
174
|
+
- Host: localhost
|
|
175
|
+
- Port: 5432
|
|
176
|
+
- Username: user
|
|
177
|
+
- Password: pass
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
This project is licensed under the Apache License 2.0.
|
|
183
|
+
|
|
184
|
+
## Contributing
|
|
185
|
+
|
|
186
|
+
If you would like to contribute to this project, please check out our [contributing guidelines](CONTRIBUTING.md).
|
|
187
|
+
|
|
188
|
+
## Issues
|
|
189
|
+
|
|
190
|
+
For any issues or bug reports, please open an issue in our [issue tracker](https://github.com/miteshbsjat/masha/issues).
|
|
191
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
masha/__init__.py,sha256=bSfOiC6oX83k99KmtzOUl97PkqiakJ_J0QCJ6THPLMI,466
|
|
2
|
+
masha/cli.py,sha256=rdBUW-CUPrJZNKujmz1UqU8z4k1ufQqPPN6XeeMBS5A,7704
|
|
3
|
+
masha/config_loader.py,sha256=Z9OVE99cayT8QiVDBV-lcZBJjG5KweMrotUicfArZuY,6020
|
|
4
|
+
masha/config_validator.py,sha256=9O75vIZU04OsCcJ_1DKIKu_WWIZFci28CYUiYNhiZCc,5136
|
|
5
|
+
masha/env_loader.py,sha256=SMoZqMSFAdohlaJ5-RqTV_v96SrWfFnemz31CPm0vns,2797
|
|
6
|
+
masha/filters/string_utils.py,sha256=MGc-N8YjMg3zz833u9uurTO05bE4I0-TAvxaDfjt5xw,530
|
|
7
|
+
masha/logger_factory.py,sha256=SvFWL-GISBQ2_0i7cHVqB_7rExG2pPFMIPlnrVhRB7A,1116
|
|
8
|
+
masha/logging.conf,sha256=lLLJ4P6kvca3jlKUkeM5HS4ADPGgUJJW2kHbK1dq-54,499
|
|
9
|
+
masha/template_renderer.py,sha256=g9jllnVz0ax6kQ-5ypi9IOKbfLrEzubM2CCH0_ZklGY,4065
|
|
10
|
+
masha/tests/maths_utils.py,sha256=IXgNZp5uFYuRJ669761GmEol6sXbvCFI7_LowT3EWvk,567
|
|
11
|
+
masha/version.py,sha256=XqGnY2IdhfQ55PqNa__EIF4JDVSJru3pTJlVFD1MEC0,45
|
|
12
|
+
masha-0.0.0.dist-info/LICENSE,sha256=vdl_OYnd2RA8DAKaIKZNyBDQPUI_qEUYRBZWzSZqSUM,10939
|
|
13
|
+
masha-0.0.0.dist-info/METADATA,sha256=7O4QE70sTLkRaxDeL2BzYXyRpyAuaUW_muGY7FPCfkY,5925
|
|
14
|
+
masha-0.0.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
|
|
15
|
+
masha-0.0.0.dist-info/entry_points.txt,sha256=_Ds-9aK5Xe-roXLO7fey8VOlHjAvdK1EN1laj48I_KA,40
|
|
16
|
+
masha-0.0.0.dist-info/RECORD,,
|