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 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()
@@ -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()