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 ADDED
File without changes
@@ -0,0 +1,14 @@
1
+ import click
2
+
3
+
4
+
5
+ @click.group()
6
+ def main():
7
+ """MXX Configuration Tool"""
8
+ pass
9
+
10
+ from mxx.cfg_tool import app # noqa
11
+ from mxx.cfg_tool.cfg import cfg # noqa
12
+ main.add_command(app)
13
+ main.add_command(cfg)
14
+
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")
@@ -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
mxx/client/__init__.py ADDED
@@ -0,0 +1,9 @@
1
+ """
2
+ MXX Client Package
3
+
4
+ Command-line client for interacting with MXX Scheduler Server.
5
+ """
6
+
7
+ from mxx.client.client import cli
8
+
9
+ __all__ = ['cli']