mcli-framework 7.0.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.
- mcli/app/chat_cmd.py +42 -0
- mcli/app/commands_cmd.py +226 -0
- mcli/app/completion_cmd.py +216 -0
- mcli/app/completion_helpers.py +288 -0
- mcli/app/cron_test_cmd.py +697 -0
- mcli/app/logs_cmd.py +419 -0
- mcli/app/main.py +492 -0
- mcli/app/model/model.py +1060 -0
- mcli/app/model_cmd.py +227 -0
- mcli/app/redis_cmd.py +269 -0
- mcli/app/video/video.py +1114 -0
- mcli/app/visual_cmd.py +303 -0
- mcli/chat/chat.py +2409 -0
- mcli/chat/command_rag.py +514 -0
- mcli/chat/enhanced_chat.py +652 -0
- mcli/chat/system_controller.py +1010 -0
- mcli/chat/system_integration.py +1016 -0
- mcli/cli.py +25 -0
- mcli/config.toml +20 -0
- mcli/lib/api/api.py +586 -0
- mcli/lib/api/daemon_client.py +203 -0
- mcli/lib/api/daemon_client_local.py +44 -0
- mcli/lib/api/daemon_decorator.py +217 -0
- mcli/lib/api/mcli_decorators.py +1032 -0
- mcli/lib/auth/auth.py +85 -0
- mcli/lib/auth/aws_manager.py +85 -0
- mcli/lib/auth/azure_manager.py +91 -0
- mcli/lib/auth/credential_manager.py +192 -0
- mcli/lib/auth/gcp_manager.py +93 -0
- mcli/lib/auth/key_manager.py +117 -0
- mcli/lib/auth/mcli_manager.py +93 -0
- mcli/lib/auth/token_manager.py +75 -0
- mcli/lib/auth/token_util.py +1011 -0
- mcli/lib/config/config.py +47 -0
- mcli/lib/discovery/__init__.py +1 -0
- mcli/lib/discovery/command_discovery.py +274 -0
- mcli/lib/erd/erd.py +1345 -0
- mcli/lib/erd/generate_graph.py +453 -0
- mcli/lib/files/files.py +76 -0
- mcli/lib/fs/fs.py +109 -0
- mcli/lib/lib.py +29 -0
- mcli/lib/logger/logger.py +611 -0
- mcli/lib/performance/optimizer.py +409 -0
- mcli/lib/performance/rust_bridge.py +502 -0
- mcli/lib/performance/uvloop_config.py +154 -0
- mcli/lib/pickles/pickles.py +50 -0
- mcli/lib/search/cached_vectorizer.py +479 -0
- mcli/lib/services/data_pipeline.py +460 -0
- mcli/lib/services/lsh_client.py +441 -0
- mcli/lib/services/redis_service.py +387 -0
- mcli/lib/shell/shell.py +137 -0
- mcli/lib/toml/toml.py +33 -0
- mcli/lib/ui/styling.py +47 -0
- mcli/lib/ui/visual_effects.py +634 -0
- mcli/lib/watcher/watcher.py +185 -0
- mcli/ml/api/app.py +215 -0
- mcli/ml/api/middleware.py +224 -0
- mcli/ml/api/routers/admin_router.py +12 -0
- mcli/ml/api/routers/auth_router.py +244 -0
- mcli/ml/api/routers/backtest_router.py +12 -0
- mcli/ml/api/routers/data_router.py +12 -0
- mcli/ml/api/routers/model_router.py +302 -0
- mcli/ml/api/routers/monitoring_router.py +12 -0
- mcli/ml/api/routers/portfolio_router.py +12 -0
- mcli/ml/api/routers/prediction_router.py +267 -0
- mcli/ml/api/routers/trade_router.py +12 -0
- mcli/ml/api/routers/websocket_router.py +76 -0
- mcli/ml/api/schemas.py +64 -0
- mcli/ml/auth/auth_manager.py +425 -0
- mcli/ml/auth/models.py +154 -0
- mcli/ml/auth/permissions.py +302 -0
- mcli/ml/backtesting/backtest_engine.py +502 -0
- mcli/ml/backtesting/performance_metrics.py +393 -0
- mcli/ml/cache.py +400 -0
- mcli/ml/cli/main.py +398 -0
- mcli/ml/config/settings.py +394 -0
- mcli/ml/configs/dvc_config.py +230 -0
- mcli/ml/configs/mlflow_config.py +131 -0
- mcli/ml/configs/mlops_manager.py +293 -0
- mcli/ml/dashboard/app.py +532 -0
- mcli/ml/dashboard/app_integrated.py +738 -0
- mcli/ml/dashboard/app_supabase.py +560 -0
- mcli/ml/dashboard/app_training.py +615 -0
- mcli/ml/dashboard/cli.py +51 -0
- mcli/ml/data_ingestion/api_connectors.py +501 -0
- mcli/ml/data_ingestion/data_pipeline.py +567 -0
- mcli/ml/data_ingestion/stream_processor.py +512 -0
- mcli/ml/database/migrations/env.py +94 -0
- mcli/ml/database/models.py +667 -0
- mcli/ml/database/session.py +200 -0
- mcli/ml/experimentation/ab_testing.py +845 -0
- mcli/ml/features/ensemble_features.py +607 -0
- mcli/ml/features/political_features.py +676 -0
- mcli/ml/features/recommendation_engine.py +809 -0
- mcli/ml/features/stock_features.py +573 -0
- mcli/ml/features/test_feature_engineering.py +346 -0
- mcli/ml/logging.py +85 -0
- mcli/ml/mlops/data_versioning.py +518 -0
- mcli/ml/mlops/experiment_tracker.py +377 -0
- mcli/ml/mlops/model_serving.py +481 -0
- mcli/ml/mlops/pipeline_orchestrator.py +614 -0
- mcli/ml/models/base_models.py +324 -0
- mcli/ml/models/ensemble_models.py +675 -0
- mcli/ml/models/recommendation_models.py +474 -0
- mcli/ml/models/test_models.py +487 -0
- mcli/ml/monitoring/drift_detection.py +676 -0
- mcli/ml/monitoring/metrics.py +45 -0
- mcli/ml/optimization/portfolio_optimizer.py +834 -0
- mcli/ml/preprocessing/data_cleaners.py +451 -0
- mcli/ml/preprocessing/feature_extractors.py +491 -0
- mcli/ml/preprocessing/ml_pipeline.py +382 -0
- mcli/ml/preprocessing/politician_trading_preprocessor.py +569 -0
- mcli/ml/preprocessing/test_preprocessing.py +294 -0
- mcli/ml/scripts/populate_sample_data.py +200 -0
- mcli/ml/tasks.py +400 -0
- mcli/ml/tests/test_integration.py +429 -0
- mcli/ml/tests/test_training_dashboard.py +387 -0
- mcli/public/oi/oi.py +15 -0
- mcli/public/public.py +4 -0
- mcli/self/self_cmd.py +1246 -0
- mcli/workflow/daemon/api_daemon.py +800 -0
- mcli/workflow/daemon/async_command_database.py +681 -0
- mcli/workflow/daemon/async_process_manager.py +591 -0
- mcli/workflow/daemon/client.py +530 -0
- mcli/workflow/daemon/commands.py +1196 -0
- mcli/workflow/daemon/daemon.py +905 -0
- mcli/workflow/daemon/daemon_api.py +59 -0
- mcli/workflow/daemon/enhanced_daemon.py +571 -0
- mcli/workflow/daemon/process_cli.py +244 -0
- mcli/workflow/daemon/process_manager.py +439 -0
- mcli/workflow/daemon/test_daemon.py +275 -0
- mcli/workflow/dashboard/dashboard_cmd.py +113 -0
- mcli/workflow/docker/docker.py +0 -0
- mcli/workflow/file/file.py +100 -0
- mcli/workflow/gcloud/config.toml +21 -0
- mcli/workflow/gcloud/gcloud.py +58 -0
- mcli/workflow/git_commit/ai_service.py +328 -0
- mcli/workflow/git_commit/commands.py +430 -0
- mcli/workflow/lsh_integration.py +355 -0
- mcli/workflow/model_service/client.py +594 -0
- mcli/workflow/model_service/download_and_run_efficient_models.py +288 -0
- mcli/workflow/model_service/lightweight_embedder.py +397 -0
- mcli/workflow/model_service/lightweight_model_server.py +714 -0
- mcli/workflow/model_service/lightweight_test.py +241 -0
- mcli/workflow/model_service/model_service.py +1955 -0
- mcli/workflow/model_service/ollama_efficient_runner.py +425 -0
- mcli/workflow/model_service/pdf_processor.py +386 -0
- mcli/workflow/model_service/test_efficient_runner.py +234 -0
- mcli/workflow/model_service/test_example.py +315 -0
- mcli/workflow/model_service/test_integration.py +131 -0
- mcli/workflow/model_service/test_new_features.py +149 -0
- mcli/workflow/openai/openai.py +99 -0
- mcli/workflow/politician_trading/commands.py +1790 -0
- mcli/workflow/politician_trading/config.py +134 -0
- mcli/workflow/politician_trading/connectivity.py +490 -0
- mcli/workflow/politician_trading/data_sources.py +395 -0
- mcli/workflow/politician_trading/database.py +410 -0
- mcli/workflow/politician_trading/demo.py +248 -0
- mcli/workflow/politician_trading/models.py +165 -0
- mcli/workflow/politician_trading/monitoring.py +413 -0
- mcli/workflow/politician_trading/scrapers.py +966 -0
- mcli/workflow/politician_trading/scrapers_california.py +412 -0
- mcli/workflow/politician_trading/scrapers_eu.py +377 -0
- mcli/workflow/politician_trading/scrapers_uk.py +350 -0
- mcli/workflow/politician_trading/scrapers_us_states.py +438 -0
- mcli/workflow/politician_trading/supabase_functions.py +354 -0
- mcli/workflow/politician_trading/workflow.py +852 -0
- mcli/workflow/registry/registry.py +180 -0
- mcli/workflow/repo/repo.py +223 -0
- mcli/workflow/scheduler/commands.py +493 -0
- mcli/workflow/scheduler/cron_parser.py +238 -0
- mcli/workflow/scheduler/job.py +182 -0
- mcli/workflow/scheduler/monitor.py +139 -0
- mcli/workflow/scheduler/persistence.py +324 -0
- mcli/workflow/scheduler/scheduler.py +679 -0
- mcli/workflow/sync/sync_cmd.py +437 -0
- mcli/workflow/sync/test_cmd.py +314 -0
- mcli/workflow/videos/videos.py +242 -0
- mcli/workflow/wakatime/wakatime.py +11 -0
- mcli/workflow/workflow.py +37 -0
- mcli_framework-7.0.0.dist-info/METADATA +479 -0
- mcli_framework-7.0.0.dist-info/RECORD +186 -0
- mcli_framework-7.0.0.dist-info/WHEEL +5 -0
- mcli_framework-7.0.0.dist-info/entry_points.txt +7 -0
- mcli_framework-7.0.0.dist-info/licenses/LICENSE +21 -0
- mcli_framework-7.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import time
|
|
4
|
+
from collections import Counter, defaultdict
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import pydot
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def load_graph_data(json_file_path):
|
|
11
|
+
"""Load the graph data from a JSON file."""
|
|
12
|
+
try:
|
|
13
|
+
with open(json_file_path, "r") as f:
|
|
14
|
+
return json.load(f)
|
|
15
|
+
except json.JSONDecodeError as e:
|
|
16
|
+
raise ValueError(f"Error parsing JSON file {json_file_path}: {e}")
|
|
17
|
+
except FileNotFoundError:
|
|
18
|
+
raise FileNotFoundError(f"Graph data file not found: {json_file_path}")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_adjacency_list(graph_data):
|
|
22
|
+
"""Build an adjacency list from the graph data."""
|
|
23
|
+
try:
|
|
24
|
+
# Extract vertices and edges
|
|
25
|
+
vertices = graph_data["graph"]["m_vertices"]["value"]
|
|
26
|
+
edges = graph_data["graph"]["m_edges"]["value"]
|
|
27
|
+
|
|
28
|
+
# Create mapping of IDs to node info
|
|
29
|
+
node_map = {node["id"]: node for node in vertices}
|
|
30
|
+
|
|
31
|
+
# Build adjacency list (directed graph)
|
|
32
|
+
adj_list = defaultdict(list)
|
|
33
|
+
for edge in edges:
|
|
34
|
+
source = edge["source"]
|
|
35
|
+
target = edge["target"]
|
|
36
|
+
adj_list[source].append(target)
|
|
37
|
+
|
|
38
|
+
return node_map, adj_list
|
|
39
|
+
except KeyError as e:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
f"Invalid graph data structure: missing key {e}. Check the format of your graph data file."
|
|
42
|
+
)
|
|
43
|
+
except TypeError as e:
|
|
44
|
+
raise ValueError(
|
|
45
|
+
f"Invalid graph data structure: {e}. Check the format of your graph data file."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def count_descendants(node_id, adj_list, visited=None):
|
|
50
|
+
"""Count the number of descendants for a node (reachable subgraph size)."""
|
|
51
|
+
if visited is None:
|
|
52
|
+
visited = set()
|
|
53
|
+
|
|
54
|
+
if node_id in visited:
|
|
55
|
+
return 0
|
|
56
|
+
|
|
57
|
+
visited.add(node_id)
|
|
58
|
+
count = 1 # Count the node itself
|
|
59
|
+
|
|
60
|
+
for neighbor in adj_list.get(node_id, []):
|
|
61
|
+
if neighbor not in visited:
|
|
62
|
+
count += count_descendants(neighbor, adj_list, visited)
|
|
63
|
+
|
|
64
|
+
return count
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def find_top_level_nodes(node_map, adj_list, top_n=10):
|
|
68
|
+
"""Find the top N nodes with the most descendants."""
|
|
69
|
+
# Count descendants for each node
|
|
70
|
+
descendant_counts = {}
|
|
71
|
+
for node_id in node_map:
|
|
72
|
+
descendant_counts[node_id] = count_descendants(node_id, adj_list)
|
|
73
|
+
|
|
74
|
+
# Sort nodes by descendant count
|
|
75
|
+
sorted_nodes = sorted(descendant_counts.items(), key=lambda x: x[1], reverse=True)
|
|
76
|
+
|
|
77
|
+
# Return top N nodes
|
|
78
|
+
return [node_id for node_id, count in sorted_nodes[:top_n]]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def build_hierarchical_graph(top_level_nodes, node_map, adj_list, max_depth=2):
|
|
82
|
+
"""Build a hierarchical graph with top-level nodes as roots."""
|
|
83
|
+
hierarchy = {}
|
|
84
|
+
|
|
85
|
+
# For each top-level node, build its subgraph
|
|
86
|
+
for node_id in top_level_nodes:
|
|
87
|
+
subgraph = {}
|
|
88
|
+
visited = set()
|
|
89
|
+
build_subgraph(node_id, node_map, adj_list, subgraph, visited, 0, max_depth)
|
|
90
|
+
hierarchy[node_id] = subgraph
|
|
91
|
+
|
|
92
|
+
return hierarchy
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def build_subgraph(node_id, node_map, adj_list, subgraph, visited, current_depth, max_depth):
|
|
96
|
+
"""Recursively build a subgraph for a node up to max_depth."""
|
|
97
|
+
if node_id in visited or current_depth > max_depth:
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
visited.add(node_id)
|
|
101
|
+
subgraph[node_id] = {"node_info": node_map[node_id], "children": {}}
|
|
102
|
+
|
|
103
|
+
if current_depth < max_depth:
|
|
104
|
+
for child_id in adj_list.get(node_id, []):
|
|
105
|
+
build_subgraph(
|
|
106
|
+
child_id,
|
|
107
|
+
node_map,
|
|
108
|
+
adj_list,
|
|
109
|
+
subgraph[node_id]["children"],
|
|
110
|
+
visited,
|
|
111
|
+
current_depth + 1,
|
|
112
|
+
max_depth,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def extract_fields_from_node(node_data):
|
|
117
|
+
"""Extract fields from node data for display in the table."""
|
|
118
|
+
fields = []
|
|
119
|
+
|
|
120
|
+
# If this is an entity node, extract fields from the data
|
|
121
|
+
if node_data.get("category") == "Entity":
|
|
122
|
+
# Get fields from node data
|
|
123
|
+
if "data" in node_data:
|
|
124
|
+
# Add package as a field
|
|
125
|
+
if "package" in node_data["data"]:
|
|
126
|
+
fields.append(("package", node_data["data"]["package"]))
|
|
127
|
+
|
|
128
|
+
# Add name if available
|
|
129
|
+
if "name" in node_data["data"]:
|
|
130
|
+
fields.append(("name", node_data["data"]["name"]))
|
|
131
|
+
|
|
132
|
+
# Add categoryMetadataIdentifier if available
|
|
133
|
+
if "categoryMetadataIdentifier" in node_data["data"]:
|
|
134
|
+
fields.append(("type", node_data["data"]["categoryMetadataIdentifier"]))
|
|
135
|
+
|
|
136
|
+
# Add id field
|
|
137
|
+
if "id" in node_data:
|
|
138
|
+
fields.append(("id", node_data["id"]))
|
|
139
|
+
|
|
140
|
+
# Add category field
|
|
141
|
+
if "category" in node_data:
|
|
142
|
+
fields.append(("category", node_data["category"]))
|
|
143
|
+
|
|
144
|
+
return fields
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def create_table_html(entity, node_data, font_size=10):
|
|
148
|
+
"""Create HTML table-style label for a node."""
|
|
149
|
+
fields = extract_fields_from_node(node_data)
|
|
150
|
+
|
|
151
|
+
# Sanitize entity name
|
|
152
|
+
entity = entity.replace(".", "_")
|
|
153
|
+
entity = entity.replace("<", "[")
|
|
154
|
+
entity = entity.replace(">", "]")
|
|
155
|
+
|
|
156
|
+
# Start the HTML table
|
|
157
|
+
html = f'<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="2">'
|
|
158
|
+
|
|
159
|
+
# Header row
|
|
160
|
+
html += f'<TR><TD PORT="header" COLSPAN="2" BGCOLOR="lightgrey"><B><FONT POINT-SIZE="{font_size+2}">{entity}</FONT></B></TD></TR>'
|
|
161
|
+
|
|
162
|
+
# Add "Data" section if there are fields
|
|
163
|
+
if fields:
|
|
164
|
+
html += f'<TR><TD COLSPAN="2" BGCOLOR="#E0E0E0"><B><FONT POINT-SIZE="{font_size}">Fields</FONT></B></TD></TR>'
|
|
165
|
+
|
|
166
|
+
# Add each field
|
|
167
|
+
for field_name, field_value in fields:
|
|
168
|
+
# Convert < and > to [ and ] for HTML compatibility
|
|
169
|
+
if field_value:
|
|
170
|
+
field_value = str(field_value).replace("<", "[").replace(">", "]")
|
|
171
|
+
html += f'<TR><TD><FONT POINT-SIZE="{font_size}">{field_name}</FONT></TD><TD><FONT POINT-SIZE="{font_size}">{field_value}</FONT></TD></TR>'
|
|
172
|
+
|
|
173
|
+
# Close the table
|
|
174
|
+
html += "</TABLE>>"
|
|
175
|
+
return html
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def create_dot_graph(hierarchy, root_node_id, max_depth=2):
|
|
179
|
+
"""Create a DOT graph visualization from the hierarchical model."""
|
|
180
|
+
graph = pydot.Dot(
|
|
181
|
+
graph_type="digraph",
|
|
182
|
+
rankdir="TB",
|
|
183
|
+
splines="ortho",
|
|
184
|
+
bgcolor="white",
|
|
185
|
+
label=f"Hierarchical Model for {root_node_id}",
|
|
186
|
+
fontsize=14,
|
|
187
|
+
labelloc="t",
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Track nodes that have been added to avoid duplicates
|
|
191
|
+
added_nodes = set()
|
|
192
|
+
# Track node depths for coloring
|
|
193
|
+
node_depths = {root_node_id: 0}
|
|
194
|
+
|
|
195
|
+
# Add nodes and edges recursively
|
|
196
|
+
add_nodes_and_edges(graph, hierarchy, root_node_id, added_nodes, node_depths, max_depth)
|
|
197
|
+
|
|
198
|
+
# Create a subgraph to force the root node to be at the top
|
|
199
|
+
root_subgraph = pydot.Subgraph(rank="min")
|
|
200
|
+
root_subgraph.add_node(pydot.Node(root_node_id))
|
|
201
|
+
graph.add_subgraph(root_subgraph)
|
|
202
|
+
|
|
203
|
+
return graph
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def add_nodes_and_edges(
|
|
207
|
+
graph, hierarchy, node_id, added_nodes, node_depths, max_depth, current_depth=0
|
|
208
|
+
):
|
|
209
|
+
"""Recursively add nodes and edges to the graph."""
|
|
210
|
+
if current_depth > max_depth or node_id in added_nodes:
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
# Find the node data
|
|
214
|
+
# For root nodes, it's in hierarchy[node_id][node_id]
|
|
215
|
+
# For other nodes, we need to look through the hierarchy to find them
|
|
216
|
+
if node_id in hierarchy:
|
|
217
|
+
node_data = hierarchy[node_id][node_id]["node_info"]
|
|
218
|
+
else:
|
|
219
|
+
# Look for this node in other nodes' children
|
|
220
|
+
for root in hierarchy:
|
|
221
|
+
found = find_node_in_hierarchy(hierarchy[root], node_id)
|
|
222
|
+
if found:
|
|
223
|
+
node_data = found
|
|
224
|
+
break
|
|
225
|
+
else:
|
|
226
|
+
# Node not found in hierarchy
|
|
227
|
+
print(f"Warning: Node {node_id} not found in hierarchy")
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
# Record node depth
|
|
231
|
+
node_depths[node_id] = current_depth
|
|
232
|
+
|
|
233
|
+
# Create HTML table label for this node
|
|
234
|
+
node_label = create_table_html(node_id, node_data)
|
|
235
|
+
|
|
236
|
+
# Determine node color based on depth
|
|
237
|
+
if current_depth == 0:
|
|
238
|
+
bg_color = "lightblue" # Root node
|
|
239
|
+
elif current_depth == 1:
|
|
240
|
+
bg_color = "#E6F5FF" # First level
|
|
241
|
+
else:
|
|
242
|
+
bg_color = "#F0F8FF" # Deeper levels
|
|
243
|
+
|
|
244
|
+
# Create the node with HTML table label
|
|
245
|
+
dot_node = pydot.Node(
|
|
246
|
+
node_id,
|
|
247
|
+
shape="none", # Using 'none' to allow custom HTML table
|
|
248
|
+
label=node_label,
|
|
249
|
+
style="filled",
|
|
250
|
+
fillcolor=bg_color,
|
|
251
|
+
margin="0",
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
graph.add_node(dot_node)
|
|
255
|
+
added_nodes.add(node_id)
|
|
256
|
+
|
|
257
|
+
# Add edges to children if not at max depth
|
|
258
|
+
if current_depth < max_depth:
|
|
259
|
+
# Get children - different path depending on whether this is a root node
|
|
260
|
+
if node_id in hierarchy:
|
|
261
|
+
children = hierarchy[node_id][node_id]["children"]
|
|
262
|
+
else:
|
|
263
|
+
# Look up this node's children
|
|
264
|
+
children_container = find_children_container(hierarchy, node_id)
|
|
265
|
+
if not children_container:
|
|
266
|
+
return
|
|
267
|
+
children = children_container
|
|
268
|
+
|
|
269
|
+
for child_id in children:
|
|
270
|
+
# Add an edge from this node to the child
|
|
271
|
+
edge = pydot.Edge(
|
|
272
|
+
node_id,
|
|
273
|
+
child_id,
|
|
274
|
+
dir="both",
|
|
275
|
+
arrowtail="none",
|
|
276
|
+
arrowhead="normal",
|
|
277
|
+
constraint=True,
|
|
278
|
+
color="black",
|
|
279
|
+
penwidth=1.5,
|
|
280
|
+
)
|
|
281
|
+
graph.add_edge(edge)
|
|
282
|
+
|
|
283
|
+
# Recursively add the child node and its children
|
|
284
|
+
if child_id not in added_nodes:
|
|
285
|
+
add_nodes_and_edges(
|
|
286
|
+
graph,
|
|
287
|
+
hierarchy,
|
|
288
|
+
child_id,
|
|
289
|
+
added_nodes,
|
|
290
|
+
node_depths,
|
|
291
|
+
max_depth,
|
|
292
|
+
current_depth + 1,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def find_node_in_hierarchy(subgraph, target_node):
|
|
297
|
+
"""Find a node's data in the hierarchy."""
|
|
298
|
+
# Check each node in the subgraph
|
|
299
|
+
for node_id, node_data in subgraph.items():
|
|
300
|
+
if node_id == target_node:
|
|
301
|
+
return node_data["node_info"]
|
|
302
|
+
|
|
303
|
+
# Recursively check children
|
|
304
|
+
if "children" in node_data:
|
|
305
|
+
result = find_node_in_hierarchy(node_data["children"], target_node)
|
|
306
|
+
if result:
|
|
307
|
+
return result
|
|
308
|
+
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def find_children_container(hierarchy, parent_node):
|
|
313
|
+
"""Find a node's children container in the hierarchy."""
|
|
314
|
+
# Check each root node
|
|
315
|
+
for root_id, root_data in hierarchy.items():
|
|
316
|
+
# Check if the target is a direct child of this root
|
|
317
|
+
if parent_node in root_data[root_id]["children"]:
|
|
318
|
+
return root_data[root_id]["children"][parent_node]["children"]
|
|
319
|
+
|
|
320
|
+
# Look in the children of this root's children
|
|
321
|
+
for child_id, child_data in root_data[root_id]["children"].items():
|
|
322
|
+
if parent_node == child_id:
|
|
323
|
+
return child_data["children"]
|
|
324
|
+
|
|
325
|
+
# Could add deeper searching if needed
|
|
326
|
+
|
|
327
|
+
return {}
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def transform_graph(graph_data, max_depth=3, top_n=5):
|
|
331
|
+
"""
|
|
332
|
+
Transform a graph into a hierarchical model based on descendant counts.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
graph_data: Dictionary containing graph data with vertices and edges
|
|
336
|
+
max_depth: Maximum depth for building the hierarchical model
|
|
337
|
+
top_n: Number of top-level nodes to include
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
tuple: (hierarchy, top_nodes) - Hierarchical model and list of top nodes
|
|
341
|
+
"""
|
|
342
|
+
# Build adjacency list from the graph data
|
|
343
|
+
node_map, adj_list = build_adjacency_list(graph_data)
|
|
344
|
+
|
|
345
|
+
# Find the top N nodes based on descendant count
|
|
346
|
+
top_nodes = find_top_level_nodes(node_map, adj_list, top_n)
|
|
347
|
+
|
|
348
|
+
# Build hierarchical graph with these as roots
|
|
349
|
+
hierarchy = build_hierarchical_graph(top_nodes, node_map, adj_list, max_depth)
|
|
350
|
+
|
|
351
|
+
# Count descendants for the top nodes
|
|
352
|
+
top_nodes_with_counts = [(node, count_descendants(node, adj_list)) for node in top_nodes]
|
|
353
|
+
|
|
354
|
+
return hierarchy, top_nodes_with_counts
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def modified_do_erd(max_depth=3, top_n=5):
|
|
358
|
+
"""
|
|
359
|
+
Generate ERD diagrams based on hierarchical model where top-level nodes
|
|
360
|
+
are those with the largest reachable subgraphs.
|
|
361
|
+
|
|
362
|
+
Args:
|
|
363
|
+
max_depth: Maximum depth for the hierarchical model (default: 3)
|
|
364
|
+
top_n: Number of top-level nodes to include (default: 5)
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
tuple: (dot_file, png_file) - Paths to the generated files for the first top node
|
|
368
|
+
"""
|
|
369
|
+
from mcli.lib.logger.logger import get_logger
|
|
370
|
+
|
|
371
|
+
logger = get_logger(__name__)
|
|
372
|
+
|
|
373
|
+
try:
|
|
374
|
+
# Define the path to the JSON file
|
|
375
|
+
json_file_path = os.path.join(
|
|
376
|
+
os.path.dirname(
|
|
377
|
+
os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
|
378
|
+
),
|
|
379
|
+
"realGraph.json",
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
logger.info(f"Using graph data from: {json_file_path}")
|
|
383
|
+
|
|
384
|
+
# Validate the file exists before attempting to load it
|
|
385
|
+
if not os.path.exists(json_file_path):
|
|
386
|
+
logger.error(f"Graph data file not found: {json_file_path}")
|
|
387
|
+
raise FileNotFoundError(f"Graph data file not found: {json_file_path}")
|
|
388
|
+
|
|
389
|
+
# Load the graph data
|
|
390
|
+
graph_data = load_graph_data(json_file_path)
|
|
391
|
+
|
|
392
|
+
# Create output directory if it doesn't exist
|
|
393
|
+
base_dir = os.path.dirname(
|
|
394
|
+
os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
|
|
395
|
+
)
|
|
396
|
+
output_dir = os.path.join(base_dir, "output")
|
|
397
|
+
if not os.path.exists(output_dir):
|
|
398
|
+
os.makedirs(output_dir)
|
|
399
|
+
logger.info(f"Created output directory: {output_dir}")
|
|
400
|
+
|
|
401
|
+
# Transform the graph into a hierarchical model
|
|
402
|
+
logger.info(f"Transforming graph with max_depth={max_depth}, top_n={top_n}")
|
|
403
|
+
hierarchy, top_nodes = transform_graph(graph_data, max_depth, top_n)
|
|
404
|
+
|
|
405
|
+
if not top_nodes:
|
|
406
|
+
logger.warning("No top-level nodes found in the graph.")
|
|
407
|
+
return None
|
|
408
|
+
|
|
409
|
+
# Generate file paths
|
|
410
|
+
timestamp = str(int(time.time() * 1000000))
|
|
411
|
+
generated_files = []
|
|
412
|
+
|
|
413
|
+
# For each top-level node, generate DOT and PNG files
|
|
414
|
+
for root_node_id, descendant_count in top_nodes:
|
|
415
|
+
try:
|
|
416
|
+
# Create the DOT graph
|
|
417
|
+
logger.info(f"Creating DOT graph for {root_node_id}")
|
|
418
|
+
dot_graph = create_dot_graph(hierarchy, root_node_id, max_depth)
|
|
419
|
+
|
|
420
|
+
# Define file paths - save to output directory
|
|
421
|
+
depth_info = f"_depth{max_depth}"
|
|
422
|
+
dot_file = os.path.join(output_dir, f"{root_node_id}{depth_info}_{timestamp}.dot")
|
|
423
|
+
png_file = os.path.join(output_dir, f"{root_node_id}{depth_info}_{timestamp}.png")
|
|
424
|
+
|
|
425
|
+
# Save the files
|
|
426
|
+
logger.info(f"Saving DOT file to {dot_file}")
|
|
427
|
+
dot_graph.write_raw(dot_file)
|
|
428
|
+
|
|
429
|
+
logger.info(f"Generating PNG file to {png_file}")
|
|
430
|
+
dot_graph.write_png(png_file)
|
|
431
|
+
|
|
432
|
+
# For return values, use just the filenames without full paths
|
|
433
|
+
dot_filename = os.path.basename(dot_file)
|
|
434
|
+
png_filename = os.path.basename(png_file)
|
|
435
|
+
|
|
436
|
+
generated_files.append((dot_filename, png_filename, root_node_id, descendant_count))
|
|
437
|
+
logger.info(
|
|
438
|
+
f"Generated graph for {root_node_id} with {descendant_count} descendants: {png_filename}"
|
|
439
|
+
)
|
|
440
|
+
except Exception as e:
|
|
441
|
+
logger.error(f"Error generating graph for node {root_node_id}: {e}")
|
|
442
|
+
|
|
443
|
+
# Return the first top-level node's file names for compatibility
|
|
444
|
+
if generated_files:
|
|
445
|
+
return generated_files[0][0], generated_files[0][1]
|
|
446
|
+
return None
|
|
447
|
+
except Exception as e:
|
|
448
|
+
logger.error(f"Error in modified_do_erd: {e}")
|
|
449
|
+
raise
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
if __name__ == "__main__":
|
|
453
|
+
modified_do_erd(max_depth=3)
|
mcli/lib/files/files.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import hashlib
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from mcli.lib.logger.logger import get_logger
|
|
6
|
+
|
|
7
|
+
logger = get_logger(__name__)
|
|
8
|
+
|
|
9
|
+
IN_MEMORY_FILE_FINGERPRINTS = {}
|
|
10
|
+
NO_CHANGE_TO_FILE = -1
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def encode_content(path):
|
|
14
|
+
"""Encode file content and track changes using MD5 fingerprints"""
|
|
15
|
+
logger.debug(f"encode_content: {path}")
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
with open(path, "rb") as file:
|
|
19
|
+
content = file.read()
|
|
20
|
+
|
|
21
|
+
# Calculate MD5 fingerprint
|
|
22
|
+
fingerprint = hashlib.md5(content).hexdigest()
|
|
23
|
+
|
|
24
|
+
# Check if file has changed
|
|
25
|
+
if IN_MEMORY_FILE_FINGERPRINTS.get(path) != fingerprint:
|
|
26
|
+
IN_MEMORY_FILE_FINGERPRINTS[path] = fingerprint
|
|
27
|
+
return base64.b64encode(content).decode("utf-8")
|
|
28
|
+
else:
|
|
29
|
+
return NO_CHANGE_TO_FILE
|
|
30
|
+
|
|
31
|
+
except Exception as e:
|
|
32
|
+
logger.error(f"Error encoding content from {path}: {e}")
|
|
33
|
+
return NO_CHANGE_TO_FILE
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def merge_txt_files(folder_path, file_type=".mcli", output_file="merged_output.txt"):
|
|
37
|
+
"""
|
|
38
|
+
Recursively merge .mcli files from the given folder and its subdirectories.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
folder_path (str): Path to the folder to start merging from
|
|
42
|
+
output_file (str, optional): Name of the output merged file. Defaults to 'merged_output.txt'.
|
|
43
|
+
"""
|
|
44
|
+
# Collect all .mcli files recursively
|
|
45
|
+
mcli_files = []
|
|
46
|
+
for root, _, files in os.walk(folder_path):
|
|
47
|
+
for file in files:
|
|
48
|
+
if file.endswith(file_type):
|
|
49
|
+
mcli_files.append(os.path.join(root, file))
|
|
50
|
+
|
|
51
|
+
# Sort files to ensure consistent ordering
|
|
52
|
+
mcli_files.sort()
|
|
53
|
+
|
|
54
|
+
# Open the output file and write contents
|
|
55
|
+
with open(output_file, "w", encoding="utf-8") as outfile:
|
|
56
|
+
for filepath in mcli_files:
|
|
57
|
+
try:
|
|
58
|
+
# Add a header to identify the source file
|
|
59
|
+
filename = os.path.basename(filepath)
|
|
60
|
+
outfile.write(f"\n--- Content from {filename} ---\n")
|
|
61
|
+
|
|
62
|
+
# Read and write content from the file
|
|
63
|
+
with open(filepath, "r", encoding="utf-8") as infile:
|
|
64
|
+
outfile.write(infile.read())
|
|
65
|
+
outfile.write("\n") # Add a newline between files
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.info(f"Error reading {filepath}: {str(e)}")
|
|
68
|
+
|
|
69
|
+
logger.info(f"Merged {len(mcli_files)} files into {output_file}")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
if __name__ == "__main__":
|
|
73
|
+
# Example usage
|
|
74
|
+
print("File utilities module")
|
|
75
|
+
print("Use encode_content() to track file changes")
|
|
76
|
+
print("Use merge_txt_files() to merge files of a specific type")
|
mcli/lib/fs/fs.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File system utilities for MCLI
|
|
3
|
+
Provides basic file system operations with path normalization
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import click
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_absolute_path(pth):
|
|
15
|
+
"""Convert path to absolute path with user expansion"""
|
|
16
|
+
pth = os.path.expanduser(pth)
|
|
17
|
+
pth = os.path.abspath(pth)
|
|
18
|
+
return pth
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def ensure_directory_exists(dirpath):
|
|
22
|
+
"""Create directory if it doesn't exist"""
|
|
23
|
+
dirpath = get_absolute_path(dirpath)
|
|
24
|
+
os.makedirs(dirpath, exist_ok=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def delete_directory(dirpath):
|
|
28
|
+
"""Delete directory if it exists"""
|
|
29
|
+
dirpath = get_absolute_path(dirpath)
|
|
30
|
+
if os.path.exists(dirpath):
|
|
31
|
+
shutil.rmtree(dirpath) # Use rmtree for non-empty directories
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def delete_file(filepath):
|
|
35
|
+
"""Delete file if it exists"""
|
|
36
|
+
filepath = get_absolute_path(filepath)
|
|
37
|
+
if os.path.exists(filepath):
|
|
38
|
+
os.remove(filepath)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_user_home():
|
|
42
|
+
"""Get user home directory"""
|
|
43
|
+
return os.path.expanduser("~")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def read_line_from_file(filepath):
|
|
47
|
+
"""Read first line from file"""
|
|
48
|
+
filepath = get_absolute_path(filepath)
|
|
49
|
+
if not os.path.exists(filepath):
|
|
50
|
+
raise Exception("File does not exist at: " + filepath)
|
|
51
|
+
with open(filepath) as f:
|
|
52
|
+
return f.readline().strip()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def copy_file(srcpath, dstpath):
|
|
56
|
+
"""Copy a file from source to destination"""
|
|
57
|
+
srcpath = get_absolute_path(srcpath)
|
|
58
|
+
dstpath = get_absolute_path(dstpath)
|
|
59
|
+
if os.path.exists(srcpath):
|
|
60
|
+
# Ensure destination directory exists
|
|
61
|
+
os.makedirs(os.path.dirname(dstpath), exist_ok=True)
|
|
62
|
+
shutil.copy2(srcpath, dstpath)
|
|
63
|
+
return True
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def file_exists(path):
|
|
68
|
+
"""Check if a file exists at the given path"""
|
|
69
|
+
path = get_absolute_path(path)
|
|
70
|
+
return os.path.exists(path)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def get_file_size(path):
|
|
74
|
+
"""Get the size of a file in bytes"""
|
|
75
|
+
path = get_absolute_path(path)
|
|
76
|
+
if file_exists(path):
|
|
77
|
+
return os.path.getsize(path)
|
|
78
|
+
return 0
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def list_files(directory, pattern="*"):
|
|
82
|
+
"""List files in a directory matching a pattern"""
|
|
83
|
+
import glob
|
|
84
|
+
|
|
85
|
+
directory = get_absolute_path(directory)
|
|
86
|
+
if os.path.exists(directory):
|
|
87
|
+
pattern_path = os.path.join(directory, pattern)
|
|
88
|
+
return glob.glob(pattern_path)
|
|
89
|
+
return []
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# Configuration management
|
|
93
|
+
CONFIG_FILE = os.path.join(click.get_app_dir("mcli"), "config.json")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def save_global_value(value):
|
|
97
|
+
"""Save a global configuration value"""
|
|
98
|
+
os.makedirs(os.path.dirname(CONFIG_FILE), exist_ok=True)
|
|
99
|
+
with open(CONFIG_FILE, "w") as f:
|
|
100
|
+
json.dump({"GLOBAL_VALUE": value}, f)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def load_global_value():
|
|
104
|
+
"""Load a global configuration value"""
|
|
105
|
+
try:
|
|
106
|
+
with open(CONFIG_FILE, "r") as f:
|
|
107
|
+
return json.load(f).get("GLOBAL_VALUE")
|
|
108
|
+
except FileNotFoundError:
|
|
109
|
+
return None
|
mcli/lib/lib.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
import click
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def import_public_module(module_name: str):
|
|
8
|
+
prefix = "mcli.public."
|
|
9
|
+
spec = importlib.util.find_spec(prefix + module_name)
|
|
10
|
+
if spec is None:
|
|
11
|
+
from mcli.lib.logger.logger import get_logger
|
|
12
|
+
|
|
13
|
+
logger = get_logger()
|
|
14
|
+
logger.error("Module is not available")
|
|
15
|
+
logger.error("Please install the module or consult current import statements")
|
|
16
|
+
sys.exit(1)
|
|
17
|
+
|
|
18
|
+
module = importlib.util.module_from_spec(spec)
|
|
19
|
+
spec.loader.exec_module(module)
|
|
20
|
+
return module
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@click.group(name="lib")
|
|
24
|
+
def lib():
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
lib()
|