mcli-framework 7.10.1__py3-none-any.whl → 7.11.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.

Potentially problematic release.


This version of mcli-framework might be problematic. Click here for more details.

Files changed (43) hide show
  1. mcli/app/commands_cmd.py +150 -58
  2. mcli/app/main.py +21 -27
  3. mcli/lib/custom_commands.py +62 -12
  4. mcli/lib/optional_deps.py +240 -0
  5. mcli/lib/paths.py +129 -5
  6. mcli/self/migrate_cmd.py +261 -0
  7. mcli/self/self_cmd.py +8 -0
  8. mcli/workflow/git_commit/ai_service.py +13 -2
  9. mcli/workflow/notebook/__init__.py +16 -0
  10. mcli/workflow/notebook/converter.py +375 -0
  11. mcli/workflow/notebook/notebook_cmd.py +441 -0
  12. mcli/workflow/notebook/schema.py +402 -0
  13. mcli/workflow/notebook/validator.py +313 -0
  14. mcli/workflow/secrets/__init__.py +4 -0
  15. mcli/workflow/secrets/secrets_cmd.py +192 -0
  16. mcli/workflow/workflow.py +35 -5
  17. {mcli_framework-7.10.1.dist-info → mcli_framework-7.11.0.dist-info}/METADATA +86 -55
  18. {mcli_framework-7.10.1.dist-info → mcli_framework-7.11.0.dist-info}/RECORD +22 -34
  19. mcli/ml/features/political_features.py +0 -677
  20. mcli/ml/preprocessing/politician_trading_preprocessor.py +0 -570
  21. mcli/workflow/politician_trading/__init__.py +0 -4
  22. mcli/workflow/politician_trading/config.py +0 -134
  23. mcli/workflow/politician_trading/connectivity.py +0 -492
  24. mcli/workflow/politician_trading/data_sources.py +0 -654
  25. mcli/workflow/politician_trading/database.py +0 -412
  26. mcli/workflow/politician_trading/demo.py +0 -249
  27. mcli/workflow/politician_trading/models.py +0 -327
  28. mcli/workflow/politician_trading/monitoring.py +0 -413
  29. mcli/workflow/politician_trading/scrapers.py +0 -1074
  30. mcli/workflow/politician_trading/scrapers_california.py +0 -434
  31. mcli/workflow/politician_trading/scrapers_corporate_registry.py +0 -797
  32. mcli/workflow/politician_trading/scrapers_eu.py +0 -376
  33. mcli/workflow/politician_trading/scrapers_free_sources.py +0 -509
  34. mcli/workflow/politician_trading/scrapers_third_party.py +0 -373
  35. mcli/workflow/politician_trading/scrapers_uk.py +0 -378
  36. mcli/workflow/politician_trading/scrapers_us_states.py +0 -471
  37. mcli/workflow/politician_trading/seed_database.py +0 -520
  38. mcli/workflow/politician_trading/supabase_functions.py +0 -354
  39. mcli/workflow/politician_trading/workflow.py +0 -879
  40. {mcli_framework-7.10.1.dist-info → mcli_framework-7.11.0.dist-info}/WHEEL +0 -0
  41. {mcli_framework-7.10.1.dist-info → mcli_framework-7.11.0.dist-info}/entry_points.txt +0 -0
  42. {mcli_framework-7.10.1.dist-info → mcli_framework-7.11.0.dist-info}/licenses/LICENSE +0 -0
  43. {mcli_framework-7.10.1.dist-info → mcli_framework-7.11.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,313 @@
1
+ """
2
+ Validation utilities for workflow notebooks.
3
+
4
+ Provides validation for:
5
+ - JSON schema compliance
6
+ - Code syntax checking
7
+ - Shell script validation
8
+ - MCLI API validation
9
+ """
10
+
11
+ import ast
12
+ import subprocess
13
+ import tempfile
14
+ from pathlib import Path
15
+ from typing import Any, Dict, List, Optional
16
+
17
+ from mcli.lib.logger.logger import get_logger
18
+
19
+ from .schema import NOTEBOOK_SCHEMA, CellLanguage, CellType, WorkflowNotebook
20
+
21
+ logger = get_logger()
22
+
23
+
24
+ class NotebookValidator:
25
+ """Validator for workflow notebooks."""
26
+
27
+ def __init__(self):
28
+ self.schema_errors: List[str] = []
29
+ self.syntax_errors: List[str] = []
30
+
31
+ def validate_schema(self, notebook: WorkflowNotebook) -> bool:
32
+ """
33
+ Validate notebook against JSON schema.
34
+
35
+ Args:
36
+ notebook: WorkflowNotebook to validate
37
+
38
+ Returns:
39
+ True if valid, False otherwise
40
+ """
41
+ self.schema_errors = []
42
+
43
+ try:
44
+ import jsonschema
45
+
46
+ data = notebook.to_dict()
47
+ jsonschema.validate(instance=data, schema=NOTEBOOK_SCHEMA)
48
+ return True
49
+
50
+ except ImportError:
51
+ # jsonschema not installed, do basic validation
52
+ logger.warning("jsonschema not installed, performing basic validation")
53
+ return self._basic_schema_validation(notebook)
54
+
55
+ except Exception as e:
56
+ self.schema_errors.append(str(e))
57
+ return False
58
+
59
+ def _basic_schema_validation(self, notebook: WorkflowNotebook) -> bool:
60
+ """Basic schema validation without jsonschema library."""
61
+ valid = True
62
+
63
+ # Check required fields
64
+ if not notebook.metadata.mcli.name:
65
+ self.schema_errors.append("Missing required field: metadata.mcli.name")
66
+ valid = False
67
+
68
+ if notebook.nbformat != 4:
69
+ self.schema_errors.append(f"Invalid nbformat: {notebook.nbformat} (expected 4)")
70
+ valid = False
71
+
72
+ # Validate cells
73
+ for i, cell in enumerate(notebook.cells):
74
+ if not cell.cell_type:
75
+ self.schema_errors.append(f"Cell {i}: Missing cell_type")
76
+ valid = False
77
+
78
+ if not cell.source and not isinstance(cell.source, (str, list)):
79
+ self.schema_errors.append(f"Cell {i}: Missing or invalid source")
80
+ valid = False
81
+
82
+ return valid
83
+
84
+ def validate_syntax(self, notebook: WorkflowNotebook) -> bool:
85
+ """
86
+ Validate code syntax in all code cells.
87
+
88
+ Args:
89
+ notebook: WorkflowNotebook to validate
90
+
91
+ Returns:
92
+ True if all code is syntactically valid, False otherwise
93
+ """
94
+ self.syntax_errors = []
95
+ all_valid = True
96
+
97
+ for i, cell in enumerate(notebook.cells):
98
+ if cell.cell_type != CellType.CODE:
99
+ continue
100
+
101
+ language = cell.language
102
+ code = cell.source_text
103
+
104
+ if language == CellLanguage.PYTHON:
105
+ if not self._validate_python_syntax(code, i):
106
+ all_valid = False
107
+
108
+ elif language in (CellLanguage.SHELL, CellLanguage.BASH, CellLanguage.ZSH):
109
+ if not self._validate_shell_syntax(code, i):
110
+ all_valid = False
111
+
112
+ return all_valid
113
+
114
+ def _validate_python_syntax(self, code: str, cell_index: int) -> bool:
115
+ """Validate Python code syntax."""
116
+ try:
117
+ ast.parse(code)
118
+ return True
119
+ except SyntaxError as e:
120
+ self.syntax_errors.append(
121
+ f"Cell {cell_index} (Python): Syntax error at line {e.lineno}: {e.msg}"
122
+ )
123
+ return False
124
+ except Exception as e:
125
+ self.syntax_errors.append(f"Cell {cell_index} (Python): {str(e)}")
126
+ return False
127
+
128
+ def _validate_shell_syntax(self, code: str, cell_index: int) -> bool:
129
+ """Validate shell script syntax using bash -n."""
130
+ try:
131
+ # Create temporary file with shell script
132
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".sh", delete=False) as f:
133
+ f.write(code)
134
+ temp_path = f.name
135
+
136
+ try:
137
+ # Use bash -n to check syntax without executing
138
+ result = subprocess.run(
139
+ ["bash", "-n", temp_path],
140
+ capture_output=True,
141
+ text=True,
142
+ timeout=5,
143
+ )
144
+
145
+ if result.returncode != 0:
146
+ error_msg = result.stderr.strip()
147
+ self.syntax_errors.append(f"Cell {cell_index} (Shell): {error_msg}")
148
+ return False
149
+
150
+ return True
151
+
152
+ finally:
153
+ # Clean up temp file
154
+ Path(temp_path).unlink(missing_ok=True)
155
+
156
+ except subprocess.TimeoutExpired:
157
+ self.syntax_errors.append(f"Cell {cell_index} (Shell): Validation timeout")
158
+ return False
159
+ except Exception as e:
160
+ self.syntax_errors.append(f"Cell {cell_index} (Shell): {str(e)}")
161
+ return False
162
+
163
+ def validate_mcli_apis(self, notebook: WorkflowNotebook) -> bool:
164
+ """
165
+ Validate MCLI API usage in code cells.
166
+
167
+ This checks for:
168
+ - Proper Click decorator usage
169
+ - MCLI library imports
170
+ - Common API patterns
171
+
172
+ Args:
173
+ notebook: WorkflowNotebook to validate
174
+
175
+ Returns:
176
+ True if API usage is valid, False otherwise
177
+ """
178
+ # TODO: Implement MCLI-specific API validation
179
+ # This could check for:
180
+ # - @click.command() or @click.group() decorators
181
+ # - Proper import statements
182
+ # - Common anti-patterns
183
+ return True
184
+
185
+ def get_all_errors(self) -> List[str]:
186
+ """Get all validation errors."""
187
+ return self.schema_errors + self.syntax_errors
188
+
189
+
190
+ class CodeLinter:
191
+ """Linter for workflow notebook code."""
192
+
193
+ def __init__(self):
194
+ self.issues: List[Dict[str, any]] = []
195
+
196
+ def lint_python(self, code: str) -> List[Dict[str, any]]:
197
+ """
198
+ Lint Python code using available linters.
199
+
200
+ Tries to use (in order):
201
+ 1. flake8
202
+ 2. pylint
203
+ 3. Basic AST-based checks
204
+
205
+ Returns:
206
+ List of lint issues
207
+ """
208
+ self.issues = []
209
+
210
+ # Try flake8
211
+ try:
212
+ import flake8.api.legacy as flake8_api
213
+
214
+ style_guide = flake8_api.get_style_guide()
215
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
216
+ f.write(code)
217
+ temp_path = f.name
218
+
219
+ try:
220
+ report = style_guide.check_files([temp_path])
221
+ # Convert flake8 results to our format
222
+ # (this is simplified - actual implementation would parse flake8 output)
223
+ if report.total_errors > 0:
224
+ self.issues.append(
225
+ {
226
+ "severity": "warning",
227
+ "message": f"Found {report.total_errors} style issues",
228
+ "line": 0,
229
+ }
230
+ )
231
+ finally:
232
+ Path(temp_path).unlink(missing_ok=True)
233
+
234
+ except ImportError:
235
+ # flake8 not available, try basic checks
236
+ self._basic_python_lint(code)
237
+
238
+ return self.issues
239
+
240
+ def _basic_python_lint(self, code: str):
241
+ """Basic Python linting using AST."""
242
+ try:
243
+ tree = ast.parse(code)
244
+
245
+ # Check for common issues
246
+ for node in ast.walk(tree):
247
+ # Check for unused imports (simplified)
248
+ if isinstance(node, ast.Import):
249
+ # TODO: Check if imports are actually used
250
+ pass
251
+
252
+ # Check for bare except
253
+ if isinstance(node, ast.ExceptHandler) and node.type is None:
254
+ self.issues.append(
255
+ {
256
+ "severity": "warning",
257
+ "message": "Bare except clause - consider specifying exception type",
258
+ "line": node.lineno,
259
+ }
260
+ )
261
+
262
+ except SyntaxError:
263
+ # Already caught by syntax validation
264
+ pass
265
+
266
+ def lint_shell(self, code: str) -> List[Dict[str, any]]:
267
+ """
268
+ Lint shell script using shellcheck if available.
269
+
270
+ Returns:
271
+ List of lint issues
272
+ """
273
+ self.issues = []
274
+
275
+ try:
276
+ # Create temporary file
277
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".sh", delete=False) as f:
278
+ f.write(code)
279
+ temp_path = f.name
280
+
281
+ try:
282
+ # Try to use shellcheck
283
+ result = subprocess.run(
284
+ ["shellcheck", "-f", "json", temp_path],
285
+ capture_output=True,
286
+ text=True,
287
+ timeout=10,
288
+ )
289
+
290
+ if result.stdout:
291
+ import json
292
+
293
+ issues = json.loads(result.stdout)
294
+ self.issues = [
295
+ {
296
+ "severity": issue.get("level", "warning"),
297
+ "message": issue.get("message", ""),
298
+ "line": issue.get("line", 0),
299
+ "code": issue.get("code", ""),
300
+ }
301
+ for issue in issues
302
+ ]
303
+
304
+ finally:
305
+ Path(temp_path).unlink(missing_ok=True)
306
+
307
+ except FileNotFoundError:
308
+ # shellcheck not installed
309
+ logger.debug("shellcheck not found, skipping shell linting")
310
+ except Exception as e:
311
+ logger.warning(f"Shell linting failed: {e}")
312
+
313
+ return self.issues
@@ -0,0 +1,4 @@
1
+ """Secrets workflow - migrated from lib.secrets"""
2
+ from .secrets_cmd import secrets
3
+
4
+ __all__ = ["secrets"]
@@ -0,0 +1,192 @@
1
+ """
2
+ Secrets workflow command - migrated from lib.secrets
3
+
4
+ This is now a workflow instead of a lib utility.
5
+ All secrets management functionality remains the same.
6
+ """
7
+
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ import click
12
+
13
+ from mcli.lib.ui.styling import error, info, success, warning
14
+
15
+ # Import from the original lib.secrets modules (keeping the implementation)
16
+ from mcli.lib.secrets.manager import SecretsManager
17
+ from mcli.lib.secrets.repl import run_repl
18
+ from mcli.lib.secrets.store import SecretsStore
19
+
20
+
21
+ @click.command(name="secrets", help="Secure secrets management with encryption and git sync")
22
+ @click.option("--repl", is_flag=True, help="Launch interactive secrets shell")
23
+ @click.option("--set", "set_secret", nargs=2, type=str, help="Set a secret (KEY VALUE)")
24
+ @click.option("--get", "get_secret", type=str, help="Get a secret value")
25
+ @click.option("--list", "list_secrets", is_flag=True, help="List all secrets")
26
+ @click.option("--delete", "delete_secret", type=str, help="Delete a secret")
27
+ @click.option("--namespace", "-n", default="default", help="Namespace for secrets")
28
+ @click.option("--show", is_flag=True, help="Show full value (not masked)")
29
+ @click.option("--export", is_flag=True, help="Export secrets as environment variables")
30
+ @click.option("--import-file", type=click.Path(exists=True), help="Import from env file")
31
+ @click.option("--store-init", is_flag=True, help="Initialize secrets store")
32
+ @click.option("--store-push", is_flag=True, help="Push secrets to store")
33
+ @click.option("--store-pull", is_flag=True, help="Pull secrets from store")
34
+ @click.option("--store-sync", is_flag=True, help="Sync secrets with store")
35
+ @click.option("--store-status", is_flag=True, help="Show store status")
36
+ @click.option("--remote", type=str, help="Git remote URL (for store init)")
37
+ @click.option("--message", "-m", type=str, help="Commit message (for store operations)")
38
+ @click.option("--output", "-o", type=click.Path(), help="Output file (for export)")
39
+ def secrets(
40
+ repl: bool,
41
+ set_secret: Optional[tuple],
42
+ get_secret: Optional[str],
43
+ list_secrets: bool,
44
+ delete_secret: Optional[str],
45
+ namespace: str,
46
+ show: bool,
47
+ export: bool,
48
+ import_file: Optional[str],
49
+ store_init: bool,
50
+ store_push: bool,
51
+ store_pull: bool,
52
+ store_sync: bool,
53
+ store_status: bool,
54
+ remote: Optional[str],
55
+ message: Optional[str],
56
+ output: Optional[str],
57
+ ):
58
+ """
59
+ Secrets management workflow - all-in-one command for managing secrets.
60
+
61
+ Examples:
62
+ mcli workflows secrets --repl # Interactive shell
63
+ mcli workflows secrets --set API_KEY abc123 # Set a secret
64
+ mcli workflows secrets --get API_KEY # Get a secret
65
+ mcli workflows secrets --list # List all secrets
66
+ mcli workflows secrets --export # Export as env vars
67
+ mcli workflows secrets --store-init # Initialize git store
68
+ """
69
+ manager = SecretsManager()
70
+
71
+ # Handle REPL
72
+ if repl:
73
+ run_repl()
74
+ return
75
+
76
+ # Handle set
77
+ if set_secret:
78
+ key, value = set_secret
79
+ try:
80
+ manager.set(key, value, namespace)
81
+ success(f"Secret '{key}' set in namespace '{namespace}'")
82
+ except Exception as e:
83
+ error(f"Failed to set secret: {e}")
84
+ return
85
+
86
+ # Handle get
87
+ if get_secret:
88
+ value = manager.get(get_secret, namespace)
89
+ if value is not None:
90
+ if show:
91
+ click.echo(value)
92
+ else:
93
+ masked = (
94
+ value[:3] + "*" * (len(value) - 6) + value[-3:]
95
+ if len(value) > 6
96
+ else "*" * len(value)
97
+ )
98
+ info(f"{get_secret} = {masked}")
99
+ info("Use --show to display the full value")
100
+ else:
101
+ warning(f"Secret '{get_secret}' not found in namespace '{namespace}'")
102
+ return
103
+
104
+ # Handle list
105
+ if list_secrets:
106
+ secrets_list = manager.list(namespace if namespace != "default" else None)
107
+ if secrets_list:
108
+ info("Secrets:")
109
+ for secret in secrets_list:
110
+ click.echo(f" • {secret}")
111
+ else:
112
+ info("No secrets found")
113
+ return
114
+
115
+ # Handle delete
116
+ if delete_secret:
117
+ if click.confirm(f"Are you sure you want to delete '{delete_secret}'?"):
118
+ if manager.delete(delete_secret, namespace):
119
+ success(f"Secret '{delete_secret}' deleted from namespace '{namespace}'")
120
+ else:
121
+ warning(f"Secret '{delete_secret}' not found in namespace '{namespace}'")
122
+ return
123
+
124
+ # Handle export
125
+ if export:
126
+ env_vars = manager.export_env(namespace if namespace != "default" else None)
127
+ if env_vars:
128
+ if output:
129
+ with open(output, "w") as f:
130
+ for key, value in env_vars.items():
131
+ f.write(f"export {key}={value}\n")
132
+ success(f"Exported {len(env_vars)} secrets to {output}")
133
+ else:
134
+ for key, value in env_vars.items():
135
+ click.echo(f"export {key}={value}")
136
+ else:
137
+ info("No secrets to export")
138
+ return
139
+
140
+ # Handle import
141
+ if import_file:
142
+ count = manager.import_env(Path(import_file), namespace)
143
+ success(f"Imported {count} secrets into namespace '{namespace}'")
144
+ return
145
+
146
+ # Store operations
147
+ store = SecretsStore()
148
+
149
+ if store_init:
150
+ store.init(remote)
151
+ success("Secrets store initialized")
152
+ return
153
+
154
+ if store_push:
155
+ store.push(manager.secrets_dir, message)
156
+ success("Secrets pushed to store")
157
+ return
158
+
159
+ if store_pull:
160
+ store.pull(manager.secrets_dir)
161
+ success("Secrets pulled from store")
162
+ return
163
+
164
+ if store_sync:
165
+ store.sync(manager.secrets_dir, message)
166
+ success("Secrets synced with store")
167
+ return
168
+
169
+ if store_status:
170
+ status = store.status()
171
+ info("Secrets Store Status:")
172
+ click.echo(f" Initialized: {status['initialized']}")
173
+ click.echo(f" Path: {status['store_path']}")
174
+
175
+ if status["initialized"]:
176
+ click.echo(f" Branch: {status['branch']}")
177
+ click.echo(f" Commit: {status['commit']}")
178
+ click.echo(f" Clean: {status['clean']}")
179
+
180
+ if status["has_remote"]:
181
+ click.echo(f" Remote: {status['remote_url']}")
182
+ else:
183
+ click.echo(" Remote: Not configured")
184
+ return
185
+
186
+ # If no action specified, show help
187
+ ctx = click.get_current_context()
188
+ click.echo(ctx.get_help())
189
+
190
+
191
+ if __name__ == "__main__":
192
+ secrets()
mcli/workflow/workflow.py CHANGED
@@ -1,5 +1,5 @@
1
1
  """
2
- Workflow command group for mcli.
2
+ Workflows command group for mcli.
3
3
 
4
4
  All workflow commands are now loaded from portable JSON files in ~/.mcli/commands/
5
5
  This provides a clean, maintainable way to manage workflow commands.
@@ -8,11 +8,41 @@ This provides a clean, maintainable way to manage workflow commands.
8
8
  import click
9
9
 
10
10
 
11
- @click.group(name="workflow")
12
- def workflow():
13
- """Workflow commands for automation, video processing, and daemon management"""
11
+ @click.group(name="workflows")
12
+ def workflows():
13
+ """Runnable workflows for automation, video processing, and daemon management"""
14
14
  pass
15
15
 
16
16
 
17
+ # Add secrets workflow
18
+ try:
19
+ from mcli.workflow.secrets.secrets_cmd import secrets
20
+
21
+ workflows.add_command(secrets)
22
+ except ImportError as e:
23
+ # Secrets workflow not available
24
+ import sys
25
+ from mcli.lib.logger.logger import get_logger
26
+
27
+ logger = get_logger()
28
+ logger.debug(f"Secrets workflow not available: {e}")
29
+
30
+ # Add notebook subcommand
31
+ try:
32
+ from mcli.workflow.notebook.notebook_cmd import notebook
33
+
34
+ workflows.add_command(notebook)
35
+ except ImportError as e:
36
+ # Notebook commands not available
37
+ import sys
38
+ from mcli.lib.logger.logger import get_logger
39
+
40
+ logger = get_logger()
41
+ logger.debug(f"Notebook commands not available: {e}")
42
+
43
+
44
+ # For backward compatibility, keep workflow as an alias
45
+ workflow = workflows
46
+
17
47
  if __name__ == "__main__":
18
- workflow()
48
+ workflows()