paramify 0.1.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.
- paramify/__init__.py +0 -0
- paramify/paramify.py +143 -0
- paramify/paramify_web.py +106 -0
- paramify/static/assets/index-C3Xrcbp_.js +6794 -0
- paramify/static/assets/index-DwXyNpx-.css +1 -0
- paramify/static/index.html +17 -0
- paramify-0.1.0.dist-info/LICENSE +21 -0
- paramify-0.1.0.dist-info/METADATA +279 -0
- paramify-0.1.0.dist-info/RECORD +11 -0
- paramify-0.1.0.dist-info/WHEEL +5 -0
- paramify-0.1.0.dist-info/top_level.txt +1 -0
paramify/__init__.py
ADDED
|
File without changes
|
paramify/paramify.py
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import yaml
|
|
3
|
+
import argparse
|
|
4
|
+
from pydantic import BaseModel, create_model, ValidationError
|
|
5
|
+
from typing import Any, Dict, Type, Union
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class Paramify:
|
|
9
|
+
def __init__(self, config: Union[Dict[str, Any], str], enable_cli: bool = True):
|
|
10
|
+
"""
|
|
11
|
+
Initialize Paramify with a dictionary, a JSON file, or a YAML file.
|
|
12
|
+
Optionally parse command-line arguments if enable_cli is True.
|
|
13
|
+
"""
|
|
14
|
+
# Load configuration from file or dictionary
|
|
15
|
+
if isinstance(config, str):
|
|
16
|
+
if config.endswith('.json'):
|
|
17
|
+
with open(config, 'r') as f:
|
|
18
|
+
config = json.load(f)
|
|
19
|
+
elif config.endswith(('.yaml', '.yml')):
|
|
20
|
+
with open(config, 'r') as f:
|
|
21
|
+
config = yaml.safe_load(f)
|
|
22
|
+
else:
|
|
23
|
+
raise ValueError("Unsupported file format. Use a JSON or YAML file.")
|
|
24
|
+
elif not isinstance(config, dict):
|
|
25
|
+
raise ValueError("Config must be a dictionary or a valid JSON/YAML file path.")
|
|
26
|
+
|
|
27
|
+
self._config = config
|
|
28
|
+
|
|
29
|
+
if not isinstance(config, dict) or 'parameters' not in config:
|
|
30
|
+
raise ValueError("Invalid configuration format. Expected a 'parameters' key.")
|
|
31
|
+
|
|
32
|
+
self._config_params: list = config['parameters']
|
|
33
|
+
|
|
34
|
+
# Dynamically create a Pydantic model
|
|
35
|
+
self.ParameterModel = self._create_model(self._config_params)
|
|
36
|
+
try:
|
|
37
|
+
self.parameters = self.ParameterModel(**{p['name']: p.get('default', None) for p in self._config_params})
|
|
38
|
+
except ValidationError as e:
|
|
39
|
+
print("Validation Error in Configuration:", e)
|
|
40
|
+
raise
|
|
41
|
+
|
|
42
|
+
# Parse CLI arguments if enabled
|
|
43
|
+
if enable_cli:
|
|
44
|
+
self._parse_cli_args()
|
|
45
|
+
|
|
46
|
+
# Dynamically create setters for each parameter
|
|
47
|
+
for param in self._config_params:
|
|
48
|
+
self._add_parameter(param['name'])
|
|
49
|
+
|
|
50
|
+
def _create_model(self, config_data: list) -> Type[BaseModel]:
|
|
51
|
+
"""
|
|
52
|
+
Dynamically create a Pydantic BaseModel based on the configuration data.
|
|
53
|
+
Fields without a default value or explicitly set to `None` are marked as optional.
|
|
54
|
+
"""
|
|
55
|
+
from typing import Optional
|
|
56
|
+
|
|
57
|
+
fields = {}
|
|
58
|
+
for param in config_data:
|
|
59
|
+
field_type = eval(param['type']) # Determine the type
|
|
60
|
+
default = param.get('default', None) # Get the default value, or None if not provided
|
|
61
|
+
|
|
62
|
+
if default is None:
|
|
63
|
+
# If no default value is provided, make the field optional
|
|
64
|
+
fields[param['name']] = (Optional[field_type], default)
|
|
65
|
+
else:
|
|
66
|
+
# Otherwise, set the field type and default value
|
|
67
|
+
fields[param['name']] = (field_type, default)
|
|
68
|
+
|
|
69
|
+
return create_model('ParameterModel', **fields)
|
|
70
|
+
|
|
71
|
+
def _add_parameter(self, name: str):
|
|
72
|
+
"""
|
|
73
|
+
Dynamically create a setter method with validation and a callback for each parameter.
|
|
74
|
+
"""
|
|
75
|
+
def setter(self, value: Any):
|
|
76
|
+
# Validate the updated value by creating a new validated model
|
|
77
|
+
try:
|
|
78
|
+
updated_params = self.parameters.dict() # Get current parameters as a dictionary
|
|
79
|
+
updated_params[name] = value # Update the parameter
|
|
80
|
+
self.parameters = self.ParameterModel(**updated_params) # Revalidate
|
|
81
|
+
except ValidationError as e:
|
|
82
|
+
raise TypeError(f"Invalid value for {name}: {e}")
|
|
83
|
+
|
|
84
|
+
# Invoke the callback for the parameter if defined
|
|
85
|
+
callback_name = f"on_{name}_set"
|
|
86
|
+
if hasattr(self, callback_name) and callable(getattr(self, callback_name)):
|
|
87
|
+
getattr(self, callback_name)(value)
|
|
88
|
+
|
|
89
|
+
# Attach the setter method to the class
|
|
90
|
+
setattr(self, f"set_{name}", setter.__get__(self))
|
|
91
|
+
|
|
92
|
+
def _parse_cli_args(self):
|
|
93
|
+
"""
|
|
94
|
+
Parse CLI arguments and update the parameters accordingly.
|
|
95
|
+
"""
|
|
96
|
+
self.parser = argparse.ArgumentParser(description=self._config.get("description", ""))
|
|
97
|
+
|
|
98
|
+
for param in self._config_params:
|
|
99
|
+
scope = param.get("scope", "all")
|
|
100
|
+
if scope not in ["all", "cli"]:
|
|
101
|
+
continue # Only include parameters with scope "all" or "cli" in the CLI
|
|
102
|
+
|
|
103
|
+
arg_name = f"--{param['name'].replace('_', '-')}"
|
|
104
|
+
param_type = param["type"]
|
|
105
|
+
|
|
106
|
+
if param_type == "bool":
|
|
107
|
+
# Use `store_true` or `store_false` for boolean arguments
|
|
108
|
+
self.parser.add_argument(
|
|
109
|
+
arg_name,
|
|
110
|
+
help=param.get("description", ""),
|
|
111
|
+
default=param.get("default", False),
|
|
112
|
+
action="store_true" if not param.get("default", False) else "store_false"
|
|
113
|
+
)
|
|
114
|
+
elif param_type == "list":
|
|
115
|
+
# Handle list arguments with nargs="+"
|
|
116
|
+
self.parser.add_argument(
|
|
117
|
+
arg_name,
|
|
118
|
+
help=param.get("description", ""),
|
|
119
|
+
nargs="+",
|
|
120
|
+
default=param.get("default", []),
|
|
121
|
+
type=str # Assume lists are of type str; adjust as needed
|
|
122
|
+
)
|
|
123
|
+
else:
|
|
124
|
+
# Add other parameter types
|
|
125
|
+
self.parser.add_argument(
|
|
126
|
+
arg_name,
|
|
127
|
+
help=param.get("description", ""),
|
|
128
|
+
default=param.get("default"),
|
|
129
|
+
type=eval(param_type) if param_type in ["int", "float", "str"] else str
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Parse arguments and update parameters
|
|
133
|
+
args = self.parser.parse_args()
|
|
134
|
+
cli_args = vars(args)
|
|
135
|
+
for name, value in cli_args.items():
|
|
136
|
+
if name in self.parameters.dict():
|
|
137
|
+
setattr(self.parameters, name, value)
|
|
138
|
+
|
|
139
|
+
def get_parameters(self) -> Dict[str, Any]:
|
|
140
|
+
"""
|
|
141
|
+
Return the current parameters and their values.
|
|
142
|
+
"""
|
|
143
|
+
return self.parameters.dict()
|
paramify/paramify_web.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import threading
|
|
3
|
+
from typing import Any, Dict, Union
|
|
4
|
+
from flask import Flask, jsonify, request, send_from_directory
|
|
5
|
+
# from flask_cors import CORS
|
|
6
|
+
|
|
7
|
+
from paramify.paramify import Paramify
|
|
8
|
+
|
|
9
|
+
class ParamifyWeb(Paramify):
|
|
10
|
+
def __init__(self,
|
|
11
|
+
config: Union[Dict[str, Any], str],
|
|
12
|
+
enable_cli: bool = True,
|
|
13
|
+
host: str = '0.0.0.0',
|
|
14
|
+
port: int = 5000):
|
|
15
|
+
"""
|
|
16
|
+
Initialize the ParamifyWeb class, set up the Flask app, and start the server in a separate thread.
|
|
17
|
+
"""
|
|
18
|
+
super().__init__(config, enable_cli) # Initialize the parent class
|
|
19
|
+
self.host = host
|
|
20
|
+
self.port = port
|
|
21
|
+
|
|
22
|
+
# Initialize Flask app with static and template folders
|
|
23
|
+
base_dir = os.path.dirname(os.path.abspath(__file__))
|
|
24
|
+
static_folder = os.path.join(base_dir, 'static/assets')
|
|
25
|
+
template_folder = os.path.join(base_dir, 'static')
|
|
26
|
+
|
|
27
|
+
# Initialize Flask app with static and template folders
|
|
28
|
+
self.app = Flask(__name__, static_folder=static_folder, template_folder=template_folder)
|
|
29
|
+
# CORS(self.app) # Enable CORS for development convenience
|
|
30
|
+
|
|
31
|
+
# Set up Flask routes
|
|
32
|
+
self._setup_routes()
|
|
33
|
+
|
|
34
|
+
# Start the Flask app in a separate thread
|
|
35
|
+
self._start_server()
|
|
36
|
+
|
|
37
|
+
def _setup_routes(self):
|
|
38
|
+
"""
|
|
39
|
+
Define Flask routes for serving the Vue app and handling parameter updates.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
# Serve the Vue app's index.html
|
|
43
|
+
@self.app.route('/')
|
|
44
|
+
@self.app.route('/<path:path>')
|
|
45
|
+
def serve_frontend(path=None):
|
|
46
|
+
"""
|
|
47
|
+
Serve static files or the Vue index.html.
|
|
48
|
+
"""
|
|
49
|
+
if path and os.path.exists(os.path.join(self.app.template_folder, path)):
|
|
50
|
+
return send_from_directory(self.app.template_folder, path)
|
|
51
|
+
return send_from_directory(self.app.template_folder, 'index.html')
|
|
52
|
+
|
|
53
|
+
# API route: Get configuration details
|
|
54
|
+
@self.app.route('/api/config', methods=['GET'])
|
|
55
|
+
def get_config():
|
|
56
|
+
"""
|
|
57
|
+
Return the configuration details (metadata and current parameter values).
|
|
58
|
+
Exclude parameters with scope "cli".
|
|
59
|
+
"""
|
|
60
|
+
# Start with the original configuration metadata
|
|
61
|
+
config_with_values = self._config.copy()
|
|
62
|
+
|
|
63
|
+
# Filter out parameters with scope "cli"
|
|
64
|
+
config_with_values['parameters'] = [
|
|
65
|
+
{
|
|
66
|
+
**param,
|
|
67
|
+
"default": self.get_parameters().get(param["name"], param.get("default"))
|
|
68
|
+
}
|
|
69
|
+
for param in config_with_values['parameters']
|
|
70
|
+
if param.get("scope", "all") != "cli"
|
|
71
|
+
]
|
|
72
|
+
|
|
73
|
+
return jsonify(config_with_values)
|
|
74
|
+
|
|
75
|
+
# API route: Update a parameter value
|
|
76
|
+
@self.app.route('/api/update', methods=['POST'])
|
|
77
|
+
def update_parameter():
|
|
78
|
+
"""
|
|
79
|
+
Update parameters dynamically based on the received data.
|
|
80
|
+
"""
|
|
81
|
+
data = request.json # Example: {'param1': False, 'param2': 42}
|
|
82
|
+
if not data or not isinstance(data, dict):
|
|
83
|
+
return jsonify({"status": "error", "message": "Invalid request format. Expected a dictionary."}), 400
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
# Iterate through the received parameters and update them
|
|
87
|
+
for name, value in data.items():
|
|
88
|
+
setter = getattr(self, f"set_{name}", None) # Dynamically find the setter
|
|
89
|
+
if not setter:
|
|
90
|
+
return jsonify({"status": "error", "message": f"Parameter '{name}' does not exist"}), 404
|
|
91
|
+
setter(value) # Call the setter to update the parameter
|
|
92
|
+
|
|
93
|
+
return jsonify({"status": "success", "message": "Parameters updated successfully"})
|
|
94
|
+
except Exception as e:
|
|
95
|
+
return jsonify({"status": "error", "message": str(e)}), 400
|
|
96
|
+
|
|
97
|
+
def _start_server(self):
|
|
98
|
+
"""
|
|
99
|
+
Start the Flask app in a separate thread to avoid blocking the main thread.
|
|
100
|
+
"""
|
|
101
|
+
server_thread = threading.Thread(
|
|
102
|
+
target=self.app.run,
|
|
103
|
+
kwargs={"host": self.host, "port": self.port, "debug": False, "use_reloader": False},
|
|
104
|
+
daemon=True # Mark the thread as a daemon thread
|
|
105
|
+
)
|
|
106
|
+
server_thread.start()
|