mxx-tool 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.
- mxx/__init__.py +0 -0
- mxx/cfg_tool/__main__.py +14 -0
- mxx/cfg_tool/app.py +117 -0
- mxx/cfg_tool/cfg.py +184 -0
- mxx/cfg_tool/registry.py +118 -0
- mxx/client/__init__.py +9 -0
- mxx/client/client.py +316 -0
- mxx/runner/builtins/__init__.py +18 -0
- mxx/runner/builtins/app_launcher.py +121 -0
- mxx/runner/builtins/lifetime.py +114 -0
- mxx/runner/builtins/mxxrun.py +158 -0
- mxx/runner/builtins/mxxset.py +171 -0
- mxx/runner/builtins/os_exec.py +78 -0
- mxx/runner/core/callstack.py +45 -0
- mxx/runner/core/config_loader.py +84 -0
- mxx/runner/core/enums.py +11 -0
- mxx/runner/core/plugin.py +23 -0
- mxx/runner/core/registry.py +101 -0
- mxx/runner/core/runner.py +128 -0
- mxx/server/__init__.py +7 -0
- mxx/server/flask_runner.py +114 -0
- mxx/server/registry.py +229 -0
- mxx/server/routes.py +370 -0
- mxx/server/schedule.py +107 -0
- mxx/server/scheduler.py +355 -0
- mxx/server/server.py +188 -0
- mxx/utils/__init__.py +7 -0
- mxx/utils/nested.py +148 -0
- mxx_tool-0.1.0.dist-info/METADATA +22 -0
- mxx_tool-0.1.0.dist-info/RECORD +34 -0
- mxx_tool-0.1.0.dist-info/WHEEL +5 -0
- mxx_tool-0.1.0.dist-info/entry_points.txt +4 -0
- mxx_tool-0.1.0.dist-info/licenses/LICENSE +21 -0
- mxx_tool-0.1.0.dist-info/top_level.txt +1 -0
mxx/__init__.py
ADDED
|
File without changes
|
mxx/cfg_tool/__main__.py
ADDED
mxx/cfg_tool/app.py
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import uuid
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from pprint import pprint
|
|
5
|
+
from .registry import load_apps_registry, save_apps_registry, get_apps_registry_paths, get_app_by_name
|
|
6
|
+
|
|
7
|
+
@click.group()
|
|
8
|
+
def app():
|
|
9
|
+
"""MXX App Registry Tool"""
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
@app.command()
|
|
13
|
+
@click.argument("path", help="Path to the application folder")
|
|
14
|
+
@click.argument("app", help="Executable name relative to the path")
|
|
15
|
+
@click.argument("cfgroute", help="Configuration route")
|
|
16
|
+
@click.option("-cfgow","--cfgoverwrite", multiple=True, help="Configuration overrides in KEY=VALUE format")
|
|
17
|
+
@click.option("--alias", multiple=True, help="Aliases for the application")
|
|
18
|
+
@click.option("-cfge", "--cfgexclude", multiple=True, help="Configuration keys to exclude")
|
|
19
|
+
def register(path, app, cfgroute, cfgoverwrite, alias, cfgexclude):
|
|
20
|
+
"""Register an application"""
|
|
21
|
+
# Load existing registries
|
|
22
|
+
apps_index, aliases_index = load_apps_registry()
|
|
23
|
+
|
|
24
|
+
# Generate a simple UID
|
|
25
|
+
simple_uid = str(uuid.uuid4())
|
|
26
|
+
|
|
27
|
+
# Parse configuration overrides into a dictionary
|
|
28
|
+
cfgow_dict = {}
|
|
29
|
+
for override in cfgoverwrite:
|
|
30
|
+
if "=" in override:
|
|
31
|
+
key, value = override.split("=", 1)
|
|
32
|
+
cfgow_dict[key] = value
|
|
33
|
+
else:
|
|
34
|
+
click.echo(f"Warning: Invalid configuration override format '{override}'. Expected KEY=VALUE", err=True)
|
|
35
|
+
|
|
36
|
+
# Create the app entry
|
|
37
|
+
app_entry = {
|
|
38
|
+
"path": str(Path(path).resolve()),
|
|
39
|
+
"app": app,
|
|
40
|
+
"cfgroute": cfgroute,
|
|
41
|
+
"cfgow": cfgow_dict
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Add configuration exclusions if provided
|
|
45
|
+
if cfgexclude:
|
|
46
|
+
app_entry["cfge"] = list(cfgexclude)
|
|
47
|
+
|
|
48
|
+
# Add to apps index
|
|
49
|
+
apps_index[simple_uid] = app_entry
|
|
50
|
+
|
|
51
|
+
# Handle aliases
|
|
52
|
+
if alias:
|
|
53
|
+
# Use provided aliases
|
|
54
|
+
for a in alias:
|
|
55
|
+
aliases_index[a] = simple_uid
|
|
56
|
+
else:
|
|
57
|
+
# Use app executable name if no aliases provided
|
|
58
|
+
app_name = Path(app).stem
|
|
59
|
+
aliases_index[app_name] = simple_uid
|
|
60
|
+
|
|
61
|
+
# Save both registries
|
|
62
|
+
save_apps_registry(apps_index, aliases_index)
|
|
63
|
+
|
|
64
|
+
# Get registry location for feedback
|
|
65
|
+
apps_index_path, _ = get_apps_registry_paths()
|
|
66
|
+
apps_dir = apps_index_path.parent
|
|
67
|
+
|
|
68
|
+
# Provide feedback
|
|
69
|
+
click.echo("Successfully registered application:")
|
|
70
|
+
click.echo(f" UID: {simple_uid}")
|
|
71
|
+
click.echo(f" Path: {app_entry['path']}")
|
|
72
|
+
click.echo(f" App: {app_entry['app']}")
|
|
73
|
+
click.echo(f" Config route: {cfgroute}")
|
|
74
|
+
if cfgow_dict:
|
|
75
|
+
click.echo(f" Config overrides: {cfgow_dict}")
|
|
76
|
+
if cfgexclude:
|
|
77
|
+
click.echo(f" Config exclusions: {list(cfgexclude)}")
|
|
78
|
+
|
|
79
|
+
if alias:
|
|
80
|
+
click.echo(f" Aliases: {', '.join(alias)}")
|
|
81
|
+
else:
|
|
82
|
+
click.echo(f" Alias: {Path(app).stem}")
|
|
83
|
+
|
|
84
|
+
click.echo(f" Registry location: {apps_dir}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@app.command()
|
|
88
|
+
@click.argument("name", required=True)
|
|
89
|
+
def get(name):
|
|
90
|
+
"""Get application configuration by name/alias"""
|
|
91
|
+
app_info = get_app_by_name(name)
|
|
92
|
+
|
|
93
|
+
if app_info:
|
|
94
|
+
click.echo(f"Configuration for '{name}':")
|
|
95
|
+
pprint(app_info)
|
|
96
|
+
else:
|
|
97
|
+
click.echo(f"Error: Application '{name}' not found in registry", err=True)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@app.command()
|
|
101
|
+
def open_folder():
|
|
102
|
+
"""Open the apps registry folder"""
|
|
103
|
+
import subprocess
|
|
104
|
+
|
|
105
|
+
apps_index_path, _ = get_apps_registry_paths()
|
|
106
|
+
apps_dir = apps_index_path.parent
|
|
107
|
+
|
|
108
|
+
# Ensure directory exists
|
|
109
|
+
apps_dir.mkdir(parents=True, exist_ok=True)
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
subprocess.run(["explorer", str(apps_dir)], check=True)
|
|
113
|
+
click.echo(f"Opened registry folder: {apps_dir}")
|
|
114
|
+
except subprocess.CalledProcessError as e:
|
|
115
|
+
click.echo(f"Error opening folder: {e}", err=True)
|
|
116
|
+
except FileNotFoundError:
|
|
117
|
+
click.echo(f"Could not open folder. Path: {apps_dir}", err=True)
|
mxx/cfg_tool/cfg.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import shutil
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from .registry import get_app_by_name, load_json_config, save_json_config
|
|
6
|
+
from mxx.utils.nested import nested_get, nested_set, nested_remove
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@click.group()
|
|
10
|
+
def cfg():
|
|
11
|
+
"""Configuration Export/Import Tools"""
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@cfg.command()
|
|
16
|
+
@click.argument("app_name")
|
|
17
|
+
@click.option("--output", "-o", help="Output name (default: uses timestamp)")
|
|
18
|
+
def export(app_name, output):
|
|
19
|
+
"""Export application configuration folder"""
|
|
20
|
+
# Get app info
|
|
21
|
+
app_info = get_app_by_name(app_name)
|
|
22
|
+
if not app_info:
|
|
23
|
+
click.echo(f"Error: Application '{app_name}' not found in registry", err=True)
|
|
24
|
+
return
|
|
25
|
+
|
|
26
|
+
app_config = app_info["config"]
|
|
27
|
+
uid = app_info["uid"]
|
|
28
|
+
|
|
29
|
+
# Get the source config folder path
|
|
30
|
+
config_folder = Path(app_config["path"]) / app_config["cfgroute"]
|
|
31
|
+
if not config_folder.exists():
|
|
32
|
+
click.echo(f"Error: Config folder not found at {config_folder}", err=True)
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
if not config_folder.is_dir():
|
|
36
|
+
click.echo(f"Error: {config_folder} is not a directory", err=True)
|
|
37
|
+
return
|
|
38
|
+
|
|
39
|
+
# Determine output location
|
|
40
|
+
exports_base = Path.home() / ".mxx" / "exports" / uid
|
|
41
|
+
exports_base.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
|
|
43
|
+
if output:
|
|
44
|
+
output_dir = exports_base / output
|
|
45
|
+
else:
|
|
46
|
+
# Use timestamp as folder name when no output name specified
|
|
47
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
48
|
+
output_dir = exports_base / timestamp
|
|
49
|
+
|
|
50
|
+
# Copy the entire config folder
|
|
51
|
+
if output_dir.exists():
|
|
52
|
+
shutil.rmtree(output_dir)
|
|
53
|
+
shutil.copytree(config_folder, output_dir)
|
|
54
|
+
|
|
55
|
+
# Process all JSON files in the copied folder to apply exclusions and remove overrides
|
|
56
|
+
processed_files = []
|
|
57
|
+
for json_file in output_dir.rglob("*.json"):
|
|
58
|
+
try:
|
|
59
|
+
config_data = load_json_config(json_file)
|
|
60
|
+
|
|
61
|
+
# Apply exclusions (remove cfge keys)
|
|
62
|
+
if "cfge" in app_config:
|
|
63
|
+
for exclude_key in app_config["cfge"]:
|
|
64
|
+
nested_remove(config_data, exclude_key)
|
|
65
|
+
|
|
66
|
+
# Apply overrides (remove cfgow keys)
|
|
67
|
+
if "cfgow" in app_config:
|
|
68
|
+
for override_key in app_config["cfgow"]:
|
|
69
|
+
nested_remove(config_data, override_key)
|
|
70
|
+
|
|
71
|
+
# Save the processed config
|
|
72
|
+
save_json_config(json_file, config_data)
|
|
73
|
+
processed_files.append(json_file.name)
|
|
74
|
+
except Exception as e:
|
|
75
|
+
click.echo(f"Warning: Could not process {json_file.name}: {e}", err=True)
|
|
76
|
+
|
|
77
|
+
click.echo(f"Exported configuration folder for '{app_name}':")
|
|
78
|
+
click.echo(f" Source: {config_folder}")
|
|
79
|
+
click.echo(f" Output: {output_dir}")
|
|
80
|
+
click.echo(f" Processed {len(processed_files)} JSON files")
|
|
81
|
+
click.echo(f" Excluded {len(app_config.get('cfge', []))} key patterns")
|
|
82
|
+
click.echo(f" Removed {len(app_config.get('cfgow', {}))} override key patterns")
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@cfg.command()
|
|
86
|
+
@click.argument("app_name")
|
|
87
|
+
@click.argument("import_folder")
|
|
88
|
+
def import_config(app_name, import_folder):
|
|
89
|
+
"""Import and merge configuration folder for an application"""
|
|
90
|
+
# Get app info
|
|
91
|
+
app_info = get_app_by_name(app_name)
|
|
92
|
+
if not app_info:
|
|
93
|
+
click.echo(f"Error: Application '{app_name}' not found in registry", err=True)
|
|
94
|
+
return
|
|
95
|
+
|
|
96
|
+
app_config = app_info["config"]
|
|
97
|
+
|
|
98
|
+
# Get the target config folder path
|
|
99
|
+
target_config_folder = Path(app_config["path"]) / app_config["cfgroute"]
|
|
100
|
+
|
|
101
|
+
# Resolve import folder path - first try relative to app's exports directory
|
|
102
|
+
uid = app_info["uid"]
|
|
103
|
+
app_exports_dir = Path.home() / ".mxx" / "exports" / uid
|
|
104
|
+
|
|
105
|
+
import_path = Path(import_folder)
|
|
106
|
+
|
|
107
|
+
# If it's not an absolute path, try relative to app's exports directory first
|
|
108
|
+
if not import_path.is_absolute():
|
|
109
|
+
relative_import_path = app_exports_dir / import_folder
|
|
110
|
+
if relative_import_path.exists() and relative_import_path.is_dir():
|
|
111
|
+
import_path = relative_import_path
|
|
112
|
+
# Otherwise keep the original path (which might be relative to current directory)
|
|
113
|
+
|
|
114
|
+
if not import_path.exists():
|
|
115
|
+
click.echo(f"Error: Import folder not found at {import_path}", err=True)
|
|
116
|
+
if not import_path.is_absolute():
|
|
117
|
+
click.echo(f" Also tried: {app_exports_dir / import_folder}", err=True)
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
if not import_path.is_dir():
|
|
121
|
+
click.echo(f"Error: {import_path} is not a directory", err=True)
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
# Ensure target config folder exists
|
|
125
|
+
target_config_folder.mkdir(parents=True, exist_ok=True)
|
|
126
|
+
|
|
127
|
+
# Process all JSON files in the import folder
|
|
128
|
+
processed_files = []
|
|
129
|
+
preserved_counts = {}
|
|
130
|
+
|
|
131
|
+
for import_json_file in import_path.rglob("*.json"):
|
|
132
|
+
try:
|
|
133
|
+
# Get relative path to maintain folder structure
|
|
134
|
+
rel_path = import_json_file.relative_to(import_path)
|
|
135
|
+
target_json_file = target_config_folder / rel_path
|
|
136
|
+
|
|
137
|
+
# Ensure target directory exists
|
|
138
|
+
target_json_file.parent.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
|
|
140
|
+
# Load import data
|
|
141
|
+
import_config_data = load_json_config(import_json_file)
|
|
142
|
+
|
|
143
|
+
# Load existing target config or create empty
|
|
144
|
+
if target_json_file.exists():
|
|
145
|
+
target_config = load_json_config(target_json_file)
|
|
146
|
+
else:
|
|
147
|
+
target_config = {}
|
|
148
|
+
|
|
149
|
+
# Preserve excluded keys (cfge) from target
|
|
150
|
+
preserved_values = {}
|
|
151
|
+
if "cfge" in app_config:
|
|
152
|
+
for exclude_key in app_config["cfge"]:
|
|
153
|
+
value = nested_get(target_config, exclude_key)
|
|
154
|
+
if value is not None:
|
|
155
|
+
preserved_values[exclude_key] = value
|
|
156
|
+
|
|
157
|
+
# Update target config with imported data
|
|
158
|
+
target_config.update(import_config_data)
|
|
159
|
+
|
|
160
|
+
# Restore preserved excluded keys
|
|
161
|
+
for key, value in preserved_values.items():
|
|
162
|
+
nested_set(target_config, key, value)
|
|
163
|
+
|
|
164
|
+
# Apply overrides (cfgow)
|
|
165
|
+
if "cfgow" in app_config:
|
|
166
|
+
for override_key, override_value in app_config["cfgow"].items():
|
|
167
|
+
nested_set(target_config, override_key, override_value)
|
|
168
|
+
|
|
169
|
+
# Save updated config
|
|
170
|
+
save_json_config(target_json_file, target_config)
|
|
171
|
+
processed_files.append(rel_path)
|
|
172
|
+
preserved_counts[str(rel_path)] = len(preserved_values)
|
|
173
|
+
|
|
174
|
+
except Exception as e:
|
|
175
|
+
click.echo(f"Warning: Could not process {import_json_file.name}: {e}", err=True)
|
|
176
|
+
|
|
177
|
+
total_preserved = sum(preserved_counts.values())
|
|
178
|
+
|
|
179
|
+
click.echo(f"Imported configuration folder for '{app_name}':")
|
|
180
|
+
click.echo(f" Import source: {import_path}")
|
|
181
|
+
click.echo(f" Target: {target_config_folder}")
|
|
182
|
+
click.echo(f" Processed {len(processed_files)} JSON files")
|
|
183
|
+
click.echo(f" Preserved {total_preserved} excluded keys across all files")
|
|
184
|
+
click.echo(f" Applied {len(app_config.get('cfgow', {}))} override patterns to all files")
|
mxx/cfg_tool/registry.py
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration file utilities for MXX cfg_tool.
|
|
3
|
+
|
|
4
|
+
Provides independent components for loading and saving JSON configuration files.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def load_json_config(file_path: Path) -> Dict[str, Any]:
|
|
13
|
+
"""
|
|
14
|
+
Load JSON configuration from file.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
file_path: Path to the JSON file
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Dictionary containing the loaded configuration
|
|
21
|
+
|
|
22
|
+
Raises:
|
|
23
|
+
FileNotFoundError: If the file doesn't exist
|
|
24
|
+
json.JSONDecodeError: If the file contains invalid JSON
|
|
25
|
+
"""
|
|
26
|
+
if not file_path.exists():
|
|
27
|
+
return {}
|
|
28
|
+
|
|
29
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
30
|
+
return json.load(f)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def save_json_config(file_path: Path, config: Dict[str, Any]) -> None:
|
|
34
|
+
"""
|
|
35
|
+
Save configuration to JSON file.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
file_path: Path where to save the JSON file
|
|
39
|
+
config: Dictionary to save as JSON
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
OSError: If the file cannot be written
|
|
43
|
+
"""
|
|
44
|
+
# Ensure parent directory exists
|
|
45
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
|
|
47
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
48
|
+
json.dump(config, f, indent=2, ensure_ascii=False)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def get_apps_registry_paths() -> tuple[Path, Path]:
|
|
52
|
+
"""
|
|
53
|
+
Get the standard paths for apps registry files.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Tuple of (apps_index_path, aliases_index_path)
|
|
57
|
+
"""
|
|
58
|
+
apps_dir = Path.home() / ".mxx" / "apps"
|
|
59
|
+
apps_index_path = apps_dir / "apps.json"
|
|
60
|
+
aliases_index_path = apps_dir / "aliases.json"
|
|
61
|
+
|
|
62
|
+
return apps_index_path, aliases_index_path
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def load_apps_registry() -> tuple[Dict[str, Any], Dict[str, str]]:
|
|
66
|
+
"""
|
|
67
|
+
Load both apps and aliases registries.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Tuple of (apps_index, aliases_index)
|
|
71
|
+
"""
|
|
72
|
+
apps_index_path, aliases_index_path = get_apps_registry_paths()
|
|
73
|
+
|
|
74
|
+
apps_index = load_json_config(apps_index_path)
|
|
75
|
+
aliases_index = load_json_config(aliases_index_path)
|
|
76
|
+
|
|
77
|
+
return apps_index, aliases_index
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def save_apps_registry(apps_index: Dict[str, Any], aliases_index: Dict[str, str]) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Save both apps and aliases registries.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
apps_index: Apps registry dictionary
|
|
86
|
+
aliases_index: Aliases registry dictionary
|
|
87
|
+
"""
|
|
88
|
+
apps_index_path, aliases_index_path = get_apps_registry_paths()
|
|
89
|
+
|
|
90
|
+
save_json_config(apps_index_path, apps_index)
|
|
91
|
+
save_json_config(aliases_index_path, aliases_index)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def get_app_by_name(name: str) -> Dict[str, Any] | None:
|
|
95
|
+
"""
|
|
96
|
+
Get application configuration by name/alias.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
name: Application name or alias
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Dictionary containing app info with keys: name, uid, config
|
|
103
|
+
Returns None if app not found
|
|
104
|
+
"""
|
|
105
|
+
apps_index, aliases_index = load_apps_registry()
|
|
106
|
+
|
|
107
|
+
# Check if name is an alias
|
|
108
|
+
if name in aliases_index:
|
|
109
|
+
uid = aliases_index[name]
|
|
110
|
+
if uid in apps_index:
|
|
111
|
+
app_config = apps_index[uid]
|
|
112
|
+
return {
|
|
113
|
+
"name": name,
|
|
114
|
+
"uid": uid,
|
|
115
|
+
"config": app_config
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return None
|