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 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()
@@ -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,5 @@
1
+ """
2
+ masha version
3
+ """
4
+
5
+ __version__ = "0.0.0"
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ masha=masha.cli:main
3
+