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
mcli/lib/erd/erd.py
ADDED
|
@@ -0,0 +1,1345 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Generic Entity Relationship Diagram (ERD) generation utilities.
|
|
3
|
+
This module provides functions and classes for generating Entity Relationship Diagrams (ERDs)
|
|
4
|
+
from generic type metadata or from graph data files. It supports both MCLI-specific type
|
|
5
|
+
systems and generic type system interfaces.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import time
|
|
11
|
+
from abc import ABC, abstractmethod
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Dict, List, Optional, Protocol, Set, Tuple, Union
|
|
14
|
+
from urllib.request import urlopen
|
|
15
|
+
|
|
16
|
+
import click
|
|
17
|
+
import pydot
|
|
18
|
+
|
|
19
|
+
from mcli.lib.auth.mcli_manager import MCLIManager
|
|
20
|
+
from mcli.lib.logger.logger import get_logger
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class TypeSystem(Protocol):
|
|
24
|
+
"""Protocol for generic type system interface."""
|
|
25
|
+
|
|
26
|
+
def get_type(self, name: str) -> Any:
|
|
27
|
+
"""Get a type by name."""
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
def get_all_types(self) -> List[str]:
|
|
31
|
+
"""Get all available type names."""
|
|
32
|
+
...
|
|
33
|
+
|
|
34
|
+
def get_package_types(self, package_name: str) -> List[str]:
|
|
35
|
+
"""Get all types in a specific package."""
|
|
36
|
+
...
|
|
37
|
+
|
|
38
|
+
def create_type_metadata(self, type_obj: Any) -> "TypeMetadata":
|
|
39
|
+
"""Create type metadata from a type object."""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TypeMetadata(Protocol):
|
|
44
|
+
"""Protocol for type metadata interface."""
|
|
45
|
+
|
|
46
|
+
def get_name(self) -> str:
|
|
47
|
+
"""Get the type name."""
|
|
48
|
+
...
|
|
49
|
+
|
|
50
|
+
def get_fields(self) -> Dict[str, Any]:
|
|
51
|
+
"""Get field definitions."""
|
|
52
|
+
...
|
|
53
|
+
|
|
54
|
+
def get_methods(self) -> List[str]:
|
|
55
|
+
"""Get method names."""
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
def get_related_types(self) -> Set[str]:
|
|
59
|
+
"""Get names of related types."""
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class MCLITypeSystem:
|
|
64
|
+
"""MCLI-specific implementation of TypeSystem."""
|
|
65
|
+
|
|
66
|
+
def __init__(self, mcli_obj):
|
|
67
|
+
self.mcli_obj = mcli_obj
|
|
68
|
+
|
|
69
|
+
def get_type(self, name: str) -> Any:
|
|
70
|
+
"""Get a type by name from MCLI namespace."""
|
|
71
|
+
if "." in name:
|
|
72
|
+
parts = name.split(".")
|
|
73
|
+
current_obj = self.mcli_obj
|
|
74
|
+
for part in parts:
|
|
75
|
+
current_obj = getattr(current_obj, part)
|
|
76
|
+
return current_obj
|
|
77
|
+
else:
|
|
78
|
+
return getattr(self.mcli_obj, name)
|
|
79
|
+
|
|
80
|
+
def get_all_types(self) -> List[str]:
|
|
81
|
+
"""Get all available type names from MCLI namespace."""
|
|
82
|
+
type_names = []
|
|
83
|
+
for attr_name in dir(self.mcli_obj):
|
|
84
|
+
if not attr_name.startswith("_") and attr_name not in ERD.system_types:
|
|
85
|
+
try:
|
|
86
|
+
attr = getattr(self.mcli_obj, attr_name)
|
|
87
|
+
if hasattr(attr, "meta") and callable(attr.meta):
|
|
88
|
+
type_names.append(attr_name)
|
|
89
|
+
except:
|
|
90
|
+
pass
|
|
91
|
+
return type_names
|
|
92
|
+
|
|
93
|
+
def get_package_types(self, package_name: str) -> List[str]:
|
|
94
|
+
"""Get all types in a specific package from MCLI namespace."""
|
|
95
|
+
type_names = []
|
|
96
|
+
try:
|
|
97
|
+
pkg = getattr(self.mcli_obj, package_name)
|
|
98
|
+
for attr_name in dir(pkg):
|
|
99
|
+
if attr_name.startswith("_"):
|
|
100
|
+
continue
|
|
101
|
+
try:
|
|
102
|
+
attr = getattr(pkg, attr_name)
|
|
103
|
+
if hasattr(attr, "meta") and callable(attr.meta):
|
|
104
|
+
type_names.append(f"{package_name}.{attr_name}")
|
|
105
|
+
except:
|
|
106
|
+
pass
|
|
107
|
+
except Exception:
|
|
108
|
+
pass
|
|
109
|
+
return type_names
|
|
110
|
+
|
|
111
|
+
def create_type_metadata(self, type_obj: Any) -> TypeMetadata:
|
|
112
|
+
"""Create MCLI type metadata from a type object."""
|
|
113
|
+
return MCLITypeMetadata(type_obj)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class MCLITypeMetadata:
|
|
117
|
+
"""MCLI-specific implementation of TypeMetadata."""
|
|
118
|
+
|
|
119
|
+
def __init__(self, mcli_type):
|
|
120
|
+
self.mcli_type = mcli_type
|
|
121
|
+
self._meta = mcli_type.meta()
|
|
122
|
+
|
|
123
|
+
def get_name(self) -> str:
|
|
124
|
+
"""Get the type name."""
|
|
125
|
+
return getattr(self.mcli_type, "name", lambda: str(self.mcli_type))()
|
|
126
|
+
|
|
127
|
+
def get_fields(self) -> Dict[str, Any]:
|
|
128
|
+
"""Get field definitions from MCLI type metadata."""
|
|
129
|
+
fields = {}
|
|
130
|
+
fts = self._meta.fieldTypesByName()
|
|
131
|
+
for name, ft in fts.items():
|
|
132
|
+
if name not in ERD.black_list:
|
|
133
|
+
fields[name] = ft
|
|
134
|
+
return fields
|
|
135
|
+
|
|
136
|
+
def get_methods(self) -> List[str]:
|
|
137
|
+
"""Get method names from MCLI type."""
|
|
138
|
+
methods = []
|
|
139
|
+
for name in dir(self.mcli_type):
|
|
140
|
+
if name.startswith("_") or name in ERD.black_list:
|
|
141
|
+
continue
|
|
142
|
+
attr = getattr(self.mcli_type, name)
|
|
143
|
+
if callable(attr):
|
|
144
|
+
methods.append(name)
|
|
145
|
+
return sorted(methods)
|
|
146
|
+
|
|
147
|
+
def get_related_types(self) -> Set[str]:
|
|
148
|
+
"""Get names of related types from MCLI type metadata."""
|
|
149
|
+
related_types = set()
|
|
150
|
+
fts = self._meta.fieldTypesByName()
|
|
151
|
+
|
|
152
|
+
for name, ft in fts.items():
|
|
153
|
+
if name in ERD.black_list:
|
|
154
|
+
continue
|
|
155
|
+
try:
|
|
156
|
+
vt = ft.valueType()
|
|
157
|
+
if hasattr(vt, "elementType"):
|
|
158
|
+
if vt.elementType.isReference():
|
|
159
|
+
related_types.add(vt.elementType.name)
|
|
160
|
+
else:
|
|
161
|
+
if vt.isReference():
|
|
162
|
+
related_types.add(vt.name)
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
|
|
166
|
+
return related_types
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
logger = get_logger(__name__)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class ERD:
|
|
173
|
+
"""
|
|
174
|
+
ERD generation utility class.
|
|
175
|
+
"""
|
|
176
|
+
|
|
177
|
+
black_list = [
|
|
178
|
+
"meta",
|
|
179
|
+
"typeWithBindings",
|
|
180
|
+
"subject",
|
|
181
|
+
"model",
|
|
182
|
+
"parent",
|
|
183
|
+
"itemFacility",
|
|
184
|
+
"recommendation",
|
|
185
|
+
"id",
|
|
186
|
+
"versionEdits",
|
|
187
|
+
"name",
|
|
188
|
+
"unit",
|
|
189
|
+
"typeIdent",
|
|
190
|
+
"version",
|
|
191
|
+
"unit",
|
|
192
|
+
"value",
|
|
193
|
+
"currency",
|
|
194
|
+
"updatedBy",
|
|
195
|
+
"status",
|
|
196
|
+
]
|
|
197
|
+
|
|
198
|
+
system_types = [
|
|
199
|
+
"Object",
|
|
200
|
+
"Type",
|
|
201
|
+
"ArrayType",
|
|
202
|
+
"BooleanType",
|
|
203
|
+
"DateTimeType",
|
|
204
|
+
"DoubleType",
|
|
205
|
+
"IntegerType",
|
|
206
|
+
"StringType",
|
|
207
|
+
]
|
|
208
|
+
|
|
209
|
+
include_list = [
|
|
210
|
+
"id",
|
|
211
|
+
"name",
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
@staticmethod
|
|
215
|
+
def get_relevant_types(type_metadata: TypeMetadata) -> Set[str]:
|
|
216
|
+
"""Get relevant reference types from type metadata."""
|
|
217
|
+
return type_metadata.get_related_types()
|
|
218
|
+
|
|
219
|
+
@staticmethod
|
|
220
|
+
def get_pkg_types(type_system: TypeSystem, pkg_name: Optional[str] = None) -> Set[str]:
|
|
221
|
+
"""Get all types from a package or root namespace."""
|
|
222
|
+
if pkg_name:
|
|
223
|
+
return set(type_system.get_package_types(pkg_name))
|
|
224
|
+
else:
|
|
225
|
+
return set(type_system.get_all_types())
|
|
226
|
+
|
|
227
|
+
@staticmethod
|
|
228
|
+
def get_entity_methods(type_metadata: TypeMetadata) -> List[str]:
|
|
229
|
+
"""Get methods of a type."""
|
|
230
|
+
return type_metadata.get_methods()
|
|
231
|
+
|
|
232
|
+
@staticmethod
|
|
233
|
+
def add_entity(entities: Dict[str, Dict], type_name: str, type_system: TypeSystem):
|
|
234
|
+
"""Add entity to the ERD using generic type system."""
|
|
235
|
+
try:
|
|
236
|
+
type_obj = type_system.get_type(type_name)
|
|
237
|
+
type_metadata = type_system.create_type_metadata(type_obj)
|
|
238
|
+
except Exception as e:
|
|
239
|
+
logger.warning(f"Could not load type {type_name}: {e}")
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
fields = type_metadata.get_fields()
|
|
243
|
+
entries = []
|
|
244
|
+
|
|
245
|
+
# Convert field metadata to display format
|
|
246
|
+
for name, field_metadata in fields.items():
|
|
247
|
+
try:
|
|
248
|
+
# Try to extract type information for display
|
|
249
|
+
if hasattr(field_metadata, "valueType"):
|
|
250
|
+
vt = field_metadata.valueType()
|
|
251
|
+
if hasattr(vt, "elementType"):
|
|
252
|
+
field_type = vt.elementType.name
|
|
253
|
+
label_ = f"[{field_type}]"
|
|
254
|
+
else:
|
|
255
|
+
label_ = getattr(vt, "name", str(vt))
|
|
256
|
+
else:
|
|
257
|
+
label_ = str(field_metadata)
|
|
258
|
+
except:
|
|
259
|
+
label_ = str(field_metadata)
|
|
260
|
+
|
|
261
|
+
entries.append((name, label_))
|
|
262
|
+
|
|
263
|
+
entries = sorted(entries, key=lambda x: x[1])
|
|
264
|
+
entities[type_name] = {"fields": entries, "methods": {}}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def do_erd(max_depth=1, type_system: Optional[TypeSystem] = None):
|
|
268
|
+
"""Generate an ERD (Entity Relationship Diagram) for a MCLI type.
|
|
269
|
+
|
|
270
|
+
This function now has two modes:
|
|
271
|
+
1. Traditional mode: Connects to a MCLI cluster and generates ERD based on type metadata
|
|
272
|
+
2. Offline mode: Uses realGraph.json to create a hierarchical model based on reachable subgraphs
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
max_depth: Maximum depth of relationships to include in the diagram
|
|
276
|
+
"""
|
|
277
|
+
# Ask the user which mode they prefer
|
|
278
|
+
import click
|
|
279
|
+
import pydot
|
|
280
|
+
|
|
281
|
+
logger.info("do_erd")
|
|
282
|
+
|
|
283
|
+
max_depth = int(max_depth)
|
|
284
|
+
|
|
285
|
+
mode = click.prompt(
|
|
286
|
+
"Do you want to generate an ERD using a MCLI cluster or using the realGraph.json file?",
|
|
287
|
+
type=click.Choice(["mcli", "realGraph"]),
|
|
288
|
+
default="realGraph",
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
if mode == "realGraph":
|
|
292
|
+
# Use the new hierarchical model approach
|
|
293
|
+
try:
|
|
294
|
+
# Import the modified_do_erd function from generate_graph
|
|
295
|
+
from mcli.app.main.generate_graph import modified_do_erd
|
|
296
|
+
|
|
297
|
+
logger.info("Generating ERD using realGraph.json...")
|
|
298
|
+
result = modified_do_erd(max_depth=max_depth)
|
|
299
|
+
if result:
|
|
300
|
+
logger.info(f"Successfully generated ERD with realGraph.json approach: {result}")
|
|
301
|
+
return result
|
|
302
|
+
else:
|
|
303
|
+
logger.warning(
|
|
304
|
+
"Failed to generate ERD with realGraph.json approach, falling back to MCLI connection method."
|
|
305
|
+
)
|
|
306
|
+
mode = "mcli"
|
|
307
|
+
except Exception as e:
|
|
308
|
+
logger.error(f"Error generating ERD from realGraph.json: {e}")
|
|
309
|
+
logger.info("Falling back to traditional MCLI connection method...")
|
|
310
|
+
# Fall back to traditional method
|
|
311
|
+
mode = "mcli"
|
|
312
|
+
|
|
313
|
+
if mode == "mcli":
|
|
314
|
+
# Original ERD generation logic
|
|
315
|
+
env_url = click.prompt("Please provide your environment url - no trailing slash")
|
|
316
|
+
|
|
317
|
+
mcli_mngr = MCLIManager(env_url=env_url)
|
|
318
|
+
mcli = mcli_mngr.mcli_as_basic_user()
|
|
319
|
+
|
|
320
|
+
# Create generic type system adapter
|
|
321
|
+
if type_system is None:
|
|
322
|
+
type_system = MCLITypeSystem(mcli)
|
|
323
|
+
|
|
324
|
+
# Ask if the user wants to generate an ERD for a specific type or a package
|
|
325
|
+
mode = click.prompt(
|
|
326
|
+
"Do you want to generate an ERD for a specific type or a package?",
|
|
327
|
+
type=click.Choice(["type", "package", "all"]),
|
|
328
|
+
default="type",
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
root_type = None
|
|
332
|
+
pkg_types = set()
|
|
333
|
+
|
|
334
|
+
if mode == "type":
|
|
335
|
+
# Get the mcli type from user input
|
|
336
|
+
type_name = click.prompt("Please enter the MCLI type name (e.g., ReliabilityAssetCase)")
|
|
337
|
+
|
|
338
|
+
# Dynamically access the mcli namespace using getattr
|
|
339
|
+
try:
|
|
340
|
+
root_type = getattr(mcli, type_name)
|
|
341
|
+
click.echo(f"Successfully retrieved type: {type_name}")
|
|
342
|
+
click.echo(f"Type info: {type(root_type)}")
|
|
343
|
+
except AttributeError:
|
|
344
|
+
available_types = []
|
|
345
|
+
for attr_name in dir(mcli):
|
|
346
|
+
if (
|
|
347
|
+
not attr_name.startswith("_") and attr_name not in ERD.system_types
|
|
348
|
+
): # Skip private attributes and system types
|
|
349
|
+
try:
|
|
350
|
+
attr = getattr(mcli, attr_name)
|
|
351
|
+
# Check if it's a type by looking for meta method
|
|
352
|
+
if hasattr(attr, "meta") and callable(attr.meta):
|
|
353
|
+
available_types.append(attr_name)
|
|
354
|
+
except Exception:
|
|
355
|
+
pass
|
|
356
|
+
|
|
357
|
+
click.echo(f"Error: Type '{type_name}' not found in mcli namespace.")
|
|
358
|
+
if available_types:
|
|
359
|
+
click.echo("\nAvailable types you can try:")
|
|
360
|
+
for t in sorted(available_types[:20]): # Show first 20 to keep it manageable
|
|
361
|
+
click.echo(f" - {t}")
|
|
362
|
+
if len(available_types) > 20:
|
|
363
|
+
click.echo(f" ... and {len(available_types) - 20} more")
|
|
364
|
+
return
|
|
365
|
+
|
|
366
|
+
elif mode == "package":
|
|
367
|
+
# Get package name
|
|
368
|
+
pkg_name = click.prompt("Please enter the package name (e.g., Pkg)")
|
|
369
|
+
try:
|
|
370
|
+
pkg = getattr(mcli, pkg_name)
|
|
371
|
+
click.echo(f"Successfully retrieved package: {pkg_name}")
|
|
372
|
+
|
|
373
|
+
# Get all types in the package
|
|
374
|
+
pkg_types = ERD.get_pkg_types(type_system, pkg_name)
|
|
375
|
+
if not pkg_types:
|
|
376
|
+
click.echo(f"No types found in package {pkg_name}")
|
|
377
|
+
return
|
|
378
|
+
|
|
379
|
+
click.echo(f"Found {len(pkg_types)} types in package {pkg_name}")
|
|
380
|
+
|
|
381
|
+
# If there are many types, ask the user if they want to include all or select
|
|
382
|
+
if len(pkg_types) > 10:
|
|
383
|
+
include_all = click.confirm(
|
|
384
|
+
f"Package contains {len(pkg_types)} types. Include all in ERD?",
|
|
385
|
+
default=False,
|
|
386
|
+
)
|
|
387
|
+
if not include_all:
|
|
388
|
+
# Let user select a specific type as the root
|
|
389
|
+
for t in sorted(list(pkg_types)[:20]):
|
|
390
|
+
click.echo(f" - {t}")
|
|
391
|
+
if len(pkg_types) > 20:
|
|
392
|
+
click.echo(f" ... and {len(pkg_types) - 20} more")
|
|
393
|
+
|
|
394
|
+
type_name = click.prompt("Please select a type to use as the root node")
|
|
395
|
+
try:
|
|
396
|
+
type_parts = type_name.split(".")
|
|
397
|
+
if len(type_parts) > 1:
|
|
398
|
+
current_obj = mcli
|
|
399
|
+
for part in type_parts:
|
|
400
|
+
current_obj = getattr(current_obj, part)
|
|
401
|
+
root_type = current_obj
|
|
402
|
+
else:
|
|
403
|
+
root_type = getattr(getattr(mcli, pkg_name), type_name)
|
|
404
|
+
click.echo(f"Using {type_name} as root node")
|
|
405
|
+
pkg_types = {type_name} # Only include the selected type
|
|
406
|
+
except AttributeError:
|
|
407
|
+
click.echo(f"Error: Type '{type_name}' not found")
|
|
408
|
+
return
|
|
409
|
+
|
|
410
|
+
# If no root type is selected, use the first type in the package
|
|
411
|
+
if root_type is None and pkg_types:
|
|
412
|
+
first_type = sorted(list(pkg_types))[0]
|
|
413
|
+
type_parts = first_type.split(".")
|
|
414
|
+
if len(type_parts) > 1:
|
|
415
|
+
current_obj = mcli
|
|
416
|
+
for part in type_parts:
|
|
417
|
+
current_obj = getattr(current_obj, part)
|
|
418
|
+
root_type = current_obj
|
|
419
|
+
else:
|
|
420
|
+
root_type = getattr(getattr(mcli, pkg_name), first_type)
|
|
421
|
+
click.echo(f"Using {first_type} as root node")
|
|
422
|
+
except AttributeError:
|
|
423
|
+
click.echo(f"Error: Package '{pkg_name}' not found")
|
|
424
|
+
# List available packages
|
|
425
|
+
packages = []
|
|
426
|
+
for attr_name in dir(mcli):
|
|
427
|
+
if not attr_name.startswith("_") and attr_name not in ERD.system_types:
|
|
428
|
+
try:
|
|
429
|
+
attr = getattr(mcli, attr_name)
|
|
430
|
+
# Check if it might be a package by checking if it has types with meta method
|
|
431
|
+
for sub_attr_name in dir(attr):
|
|
432
|
+
if not sub_attr_name.startswith("_"):
|
|
433
|
+
try:
|
|
434
|
+
sub_attr = getattr(attr, sub_attr_name)
|
|
435
|
+
if hasattr(sub_attr, "meta") and callable(sub_attr.meta):
|
|
436
|
+
packages.append(attr_name)
|
|
437
|
+
break
|
|
438
|
+
except:
|
|
439
|
+
pass
|
|
440
|
+
except:
|
|
441
|
+
pass
|
|
442
|
+
|
|
443
|
+
if packages:
|
|
444
|
+
click.echo("\nAvailable packages you can try:")
|
|
445
|
+
for p in sorted(packages):
|
|
446
|
+
click.echo(f" - {p}")
|
|
447
|
+
return
|
|
448
|
+
|
|
449
|
+
elif mode == "all":
|
|
450
|
+
# Get all types from the root namespace
|
|
451
|
+
pkg_types = ERD.get_pkg_types(type_system)
|
|
452
|
+
if not pkg_types:
|
|
453
|
+
click.echo("No types found in root namespace")
|
|
454
|
+
return
|
|
455
|
+
|
|
456
|
+
click.echo(f"Found {len(pkg_types)} types in root namespace")
|
|
457
|
+
|
|
458
|
+
# Let user select a specific type as the root
|
|
459
|
+
for t in sorted(list(pkg_types)[:20]):
|
|
460
|
+
click.echo(f" - {t}")
|
|
461
|
+
if len(pkg_types) > 20:
|
|
462
|
+
click.echo(f" ... and {len(pkg_types) - 20} more")
|
|
463
|
+
|
|
464
|
+
type_name = click.prompt("Please select a type to use as the root node")
|
|
465
|
+
try:
|
|
466
|
+
root_type = getattr(mcli, type_name)
|
|
467
|
+
click.echo(f"Using {type_name} as root node")
|
|
468
|
+
|
|
469
|
+
# Ask if user wants to include Pkg types too
|
|
470
|
+
include_pkg_types = click.confirm(
|
|
471
|
+
"Would you like to include types from packages as well?", default=True
|
|
472
|
+
)
|
|
473
|
+
if include_pkg_types:
|
|
474
|
+
# Get list of available packages
|
|
475
|
+
packages = []
|
|
476
|
+
for attr_name in dir(mcli):
|
|
477
|
+
if not attr_name.startswith("_") and attr_name not in ERD.system_types:
|
|
478
|
+
try:
|
|
479
|
+
attr = getattr(mcli, attr_name)
|
|
480
|
+
# Check if it might be a package by checking if it has types with meta method
|
|
481
|
+
for sub_attr_name in dir(attr):
|
|
482
|
+
if not sub_attr_name.startswith("_"):
|
|
483
|
+
try:
|
|
484
|
+
sub_attr = getattr(attr, sub_attr_name)
|
|
485
|
+
if hasattr(sub_attr, "meta") and callable(
|
|
486
|
+
sub_attr.meta
|
|
487
|
+
):
|
|
488
|
+
packages.append(attr_name)
|
|
489
|
+
break
|
|
490
|
+
except:
|
|
491
|
+
pass
|
|
492
|
+
except:
|
|
493
|
+
pass
|
|
494
|
+
|
|
495
|
+
if packages:
|
|
496
|
+
click.echo("\nAvailable packages:")
|
|
497
|
+
for p in sorted(packages):
|
|
498
|
+
click.echo(f" - {p}")
|
|
499
|
+
|
|
500
|
+
# Let user select packages to include
|
|
501
|
+
selected_pkg = click.prompt(
|
|
502
|
+
"Enter package name to include (or 'all' for all packages)",
|
|
503
|
+
default="all",
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
if selected_pkg.lower() == "all":
|
|
507
|
+
# Add types from all packages
|
|
508
|
+
for pkg_name in packages:
|
|
509
|
+
pkg_type_set = ERD.get_pkg_types(type_system, pkg_name)
|
|
510
|
+
click.echo(
|
|
511
|
+
f"Adding {len(pkg_type_set)} types from package {pkg_name}"
|
|
512
|
+
)
|
|
513
|
+
pkg_types.update(pkg_type_set)
|
|
514
|
+
else:
|
|
515
|
+
# Add types from selected package
|
|
516
|
+
if selected_pkg in packages:
|
|
517
|
+
pkg_type_set = ERD.get_pkg_types(type_system, selected_pkg)
|
|
518
|
+
click.echo(
|
|
519
|
+
f"Adding {len(pkg_type_set)} types from package {selected_pkg}"
|
|
520
|
+
)
|
|
521
|
+
pkg_types.update(pkg_type_set)
|
|
522
|
+
else:
|
|
523
|
+
click.echo(
|
|
524
|
+
f"Package '{selected_pkg}' not found, using only root namespace types"
|
|
525
|
+
)
|
|
526
|
+
except AttributeError:
|
|
527
|
+
click.echo(f"Error: Type '{type_name}' not found")
|
|
528
|
+
return
|
|
529
|
+
|
|
530
|
+
if root_type is None:
|
|
531
|
+
click.echo("Error: No root type selected for ERD generation")
|
|
532
|
+
return
|
|
533
|
+
|
|
534
|
+
# Initialize a dictionary to track processed types
|
|
535
|
+
processed_types = set()
|
|
536
|
+
# Initialize a dictionary to map types to their depth
|
|
537
|
+
type_depth = {root_type.name(): 0}
|
|
538
|
+
|
|
539
|
+
# Add the root type to processed types
|
|
540
|
+
processed_types.add(root_type.name())
|
|
541
|
+
|
|
542
|
+
# If we're including types from a package, add them to the initial list
|
|
543
|
+
if pkg_types and mode != "type":
|
|
544
|
+
click.echo(
|
|
545
|
+
f"Including {len(pkg_types)} types from {'package' if mode == 'package' else 'root namespace'}"
|
|
546
|
+
)
|
|
547
|
+
# We'll process these up to max_depth from the root type
|
|
548
|
+
|
|
549
|
+
# Prepare to process all selected types
|
|
550
|
+
# We'll need to include them all in the graph
|
|
551
|
+
additional_types_to_process = []
|
|
552
|
+
|
|
553
|
+
for type_name in pkg_types:
|
|
554
|
+
# Skip the root type
|
|
555
|
+
if type_name == root_type.name():
|
|
556
|
+
continue
|
|
557
|
+
|
|
558
|
+
try:
|
|
559
|
+
# Handle both package.Type and Type formats
|
|
560
|
+
if "." in type_name:
|
|
561
|
+
parts = type_name.split(".")
|
|
562
|
+
current_obj = mcli
|
|
563
|
+
for part in parts:
|
|
564
|
+
current_obj = getattr(current_obj, part)
|
|
565
|
+
pkg_type = current_obj
|
|
566
|
+
else:
|
|
567
|
+
pkg_type = getattr(mcli, type_name)
|
|
568
|
+
|
|
569
|
+
if hasattr(pkg_type, "meta") and callable(pkg_type.meta):
|
|
570
|
+
additional_types_to_process.append((type_name, 0)) # Start at depth 0
|
|
571
|
+
except Exception as e:
|
|
572
|
+
# Skip any type that can't be loaded
|
|
573
|
+
logger.info(f"Error loading type {type_name}: {e}")
|
|
574
|
+
|
|
575
|
+
click.echo(f"Will process {len(additional_types_to_process)} additional types")
|
|
576
|
+
|
|
577
|
+
entities = {}
|
|
578
|
+
|
|
579
|
+
ERD.add_entity(entities, root_type.name(), type_system)
|
|
580
|
+
processed_types.add(root_type.name())
|
|
581
|
+
|
|
582
|
+
# Get all related types up to max_depth
|
|
583
|
+
to_process = [(root_type.name(), 0)] # (type_name, current_depth)
|
|
584
|
+
|
|
585
|
+
# Add additional types from packages if applicable
|
|
586
|
+
if pkg_types and mode != "type" and "additional_types_to_process" in locals():
|
|
587
|
+
to_process.extend(additional_types_to_process)
|
|
588
|
+
|
|
589
|
+
while to_process:
|
|
590
|
+
current_type, current_depth = to_process.pop(0)
|
|
591
|
+
|
|
592
|
+
if current_depth >= max_depth:
|
|
593
|
+
continue
|
|
594
|
+
|
|
595
|
+
# Get relevant types for the current type
|
|
596
|
+
try:
|
|
597
|
+
current_type_obj = type_system.get_type(current_type)
|
|
598
|
+
current_type_metadata = type_system.create_type_metadata(current_type_obj)
|
|
599
|
+
|
|
600
|
+
# Add entity for this type if not already added
|
|
601
|
+
if current_type not in entities:
|
|
602
|
+
ERD.add_entity(entities, current_type, type_system)
|
|
603
|
+
processed_types.add(current_type)
|
|
604
|
+
type_depth[current_type] = current_depth
|
|
605
|
+
|
|
606
|
+
relevant_types = ERD.get_relevant_types(current_type_metadata)
|
|
607
|
+
|
|
608
|
+
for related_type in relevant_types:
|
|
609
|
+
if related_type not in processed_types:
|
|
610
|
+
ERD.add_entity(entities, related_type, type_system)
|
|
611
|
+
processed_types.add(related_type)
|
|
612
|
+
type_depth[related_type] = current_depth + 1
|
|
613
|
+
to_process.append((related_type, current_depth + 1))
|
|
614
|
+
except Exception as e:
|
|
615
|
+
logger.info(f"Error processing {current_type}: {e}")
|
|
616
|
+
|
|
617
|
+
# Create a new graph
|
|
618
|
+
graph = pydot.Dot(graph_type="digraph", rankdir="TB", splines="ortho", bgcolor="white")
|
|
619
|
+
|
|
620
|
+
# Function to create table-based node labels
|
|
621
|
+
def create_table_html(entity, entity_data, font_size=10):
|
|
622
|
+
fields = entity_data["fields"]
|
|
623
|
+
methods = entity_data["methods"]
|
|
624
|
+
|
|
625
|
+
entity = entity.replace(".", "_")
|
|
626
|
+
entity = entity.replace("<", "[")
|
|
627
|
+
entity = entity.replace(">", "]")
|
|
628
|
+
|
|
629
|
+
html = f'<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="2">'
|
|
630
|
+
html += f'<TR><TD PORT="header" COLSPAN="2" BGCOLOR="lightgrey"><B><FONT POINT-SIZE="{font_size}">{entity}</FONT></B></TD></TR>'
|
|
631
|
+
|
|
632
|
+
# Fields/Members section
|
|
633
|
+
if fields:
|
|
634
|
+
html += f'<TR><TD COLSPAN="2" BGCOLOR="#E0E0E0"><B><FONT POINT-SIZE="{font_size}">Fields</FONT></B></TD></TR>'
|
|
635
|
+
for field, type_ in fields:
|
|
636
|
+
type_ = type_.replace("<", "[")
|
|
637
|
+
type_ = type_.replace(">", "]")
|
|
638
|
+
if not type_:
|
|
639
|
+
continue
|
|
640
|
+
html += f'<TR><TD><FONT POINT-SIZE="{font_size}">{field}</FONT></TD><TD><FONT POINT-SIZE="{font_size}">{type_}</FONT></TD></TR>'
|
|
641
|
+
|
|
642
|
+
# Methods section
|
|
643
|
+
if methods:
|
|
644
|
+
html += f'<TR><TD COLSPAN="2" BGCOLOR="#E0E0E0"><B><FONT POINT-SIZE="{font_size}">Methods</FONT></B></TD></TR>'
|
|
645
|
+
for method in methods:
|
|
646
|
+
html += f'<TR><TD COLSPAN="2"><FONT POINT-SIZE="{font_size}">{method}()</FONT></TD></TR>'
|
|
647
|
+
|
|
648
|
+
html += "</TABLE>>"
|
|
649
|
+
return html
|
|
650
|
+
|
|
651
|
+
# Create nodes for all entities
|
|
652
|
+
for entity, entity_data in entities.items():
|
|
653
|
+
entity_normalized = entity.replace(".", "_")
|
|
654
|
+
|
|
655
|
+
# Create a node with table-style label showing fields and methods
|
|
656
|
+
node_label = create_table_html(entity, entity_data, font_size=10)
|
|
657
|
+
|
|
658
|
+
# Determine node color based on depth
|
|
659
|
+
node_depth = type_depth.get(entity, 0)
|
|
660
|
+
bg_color = "white" # default
|
|
661
|
+
if entity == root_type.name():
|
|
662
|
+
bg_color = "lightblue" # root node
|
|
663
|
+
elif node_depth == 1:
|
|
664
|
+
bg_color = "#E6F5FF" # light blue for first level
|
|
665
|
+
elif node_depth == 2:
|
|
666
|
+
bg_color = "#F0F8FF" # even lighter blue for second level
|
|
667
|
+
|
|
668
|
+
# Create the node
|
|
669
|
+
node = pydot.Node(
|
|
670
|
+
entity_normalized,
|
|
671
|
+
shape="none", # Using 'none' to allow custom HTML table
|
|
672
|
+
label=node_label,
|
|
673
|
+
style="filled",
|
|
674
|
+
fillcolor=bg_color,
|
|
675
|
+
margin="0",
|
|
676
|
+
)
|
|
677
|
+
graph.add_node(node)
|
|
678
|
+
|
|
679
|
+
# Track child-parent relationships dynamically
|
|
680
|
+
parent_map = {} # Maps entity -> parent
|
|
681
|
+
|
|
682
|
+
# Get all relationships and track which type belongs under which parent
|
|
683
|
+
for entity_type in processed_types:
|
|
684
|
+
if entity_type == root_type.name():
|
|
685
|
+
continue
|
|
686
|
+
|
|
687
|
+
current_mcli_type = getattr(mcli, entity_type, None)
|
|
688
|
+
if not current_mcli_type:
|
|
689
|
+
continue
|
|
690
|
+
|
|
691
|
+
try:
|
|
692
|
+
relevant_types = ERD.get_relevant_types(current_mcli_type)
|
|
693
|
+
for related_type in relevant_types:
|
|
694
|
+
if related_type not in processed_types:
|
|
695
|
+
continue
|
|
696
|
+
# Store the parent-child mapping
|
|
697
|
+
parent_map[related_type] = entity_type
|
|
698
|
+
except Exception as e:
|
|
699
|
+
logger.info(f"Error processing {entity_type}: {e}")
|
|
700
|
+
|
|
701
|
+
# Add edges based on parent-child relationships
|
|
702
|
+
for child, parent in parent_map.items():
|
|
703
|
+
child_normalized = child.replace(".", "_")
|
|
704
|
+
parent_normalized = parent.replace(".", "_")
|
|
705
|
+
|
|
706
|
+
edge = pydot.Edge(
|
|
707
|
+
parent_normalized,
|
|
708
|
+
child_normalized,
|
|
709
|
+
dir="both",
|
|
710
|
+
arrowtail="none",
|
|
711
|
+
arrowhead="normal",
|
|
712
|
+
constraint=True,
|
|
713
|
+
color="black",
|
|
714
|
+
penwidth=1.5,
|
|
715
|
+
)
|
|
716
|
+
graph.add_edge(edge)
|
|
717
|
+
|
|
718
|
+
# Set root node
|
|
719
|
+
root_type_name = root_type.name().replace(".", "_")
|
|
720
|
+
|
|
721
|
+
# Use a subgraph to force the root node to be at the center/top
|
|
722
|
+
root_subgraph = pydot.Subgraph(rank="min")
|
|
723
|
+
# We don't need to recreate the root node here as we've already created it in the loop above
|
|
724
|
+
# Just add it to the subgraph
|
|
725
|
+
root_subgraph.add_node(pydot.Node(root_type_name))
|
|
726
|
+
graph.add_subgraph(root_subgraph)
|
|
727
|
+
|
|
728
|
+
# Save the graph to a file
|
|
729
|
+
depth_info = f"_depth{max_depth}"
|
|
730
|
+
time_info = str(time.time_ns())
|
|
731
|
+
sep = "__"
|
|
732
|
+
dot_file = root_type.name() + depth_info + sep + time_info + ".dot"
|
|
733
|
+
png_file = root_type.name() + depth_info + sep + time_info + ".png"
|
|
734
|
+
graph.write_png(png_file)
|
|
735
|
+
graph.write_raw(dot_file)
|
|
736
|
+
|
|
737
|
+
logger.info(
|
|
738
|
+
f"ERD generated successfully with fields and methods. Output files: {png_file} and {dot_file}"
|
|
739
|
+
)
|
|
740
|
+
|
|
741
|
+
return dot_file, png_file
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def create_merged_erd(
|
|
745
|
+
types: List[str],
|
|
746
|
+
type_system: Optional[TypeSystem] = None,
|
|
747
|
+
mcli=None,
|
|
748
|
+
env_url: str = None,
|
|
749
|
+
max_depth: int = 2,
|
|
750
|
+
output_prefix: str = "MergedERD",
|
|
751
|
+
include_methods: bool = False,
|
|
752
|
+
) -> Tuple[str, str]:
|
|
753
|
+
"""
|
|
754
|
+
Create a merged ERD from multiple root types.
|
|
755
|
+
|
|
756
|
+
Args:
|
|
757
|
+
types: List of MCLI type names to include as root nodes
|
|
758
|
+
mcli: Optional MCLI connection object. If not provided, env_url must be provided.
|
|
759
|
+
env_url: Optional environment URL to connect to MCLI. Only needed if mcli is not provided.
|
|
760
|
+
max_depth: Maximum depth of relationships to include in the diagram
|
|
761
|
+
output_prefix: Prefix for output files
|
|
762
|
+
include_methods: Whether to include method information in the diagram
|
|
763
|
+
|
|
764
|
+
Returns:
|
|
765
|
+
Tuple of (dot_file_path, png_file_path)
|
|
766
|
+
"""
|
|
767
|
+
logger.info("create_merged_erd")
|
|
768
|
+
# Establish MCLI connection if needed
|
|
769
|
+
if type_system is None:
|
|
770
|
+
if mcli is None:
|
|
771
|
+
if env_url is None:
|
|
772
|
+
raise ValueError("Either type_system, mcli, or env_url must be provided")
|
|
773
|
+
mcli_mngr = MCLIManager(env_url=env_url)
|
|
774
|
+
mcli = mcli_mngr.mcli_as_basic_user()
|
|
775
|
+
type_system = MCLITypeSystem(mcli)
|
|
776
|
+
|
|
777
|
+
# Validate all types exist
|
|
778
|
+
root_types = []
|
|
779
|
+
for type_name in types:
|
|
780
|
+
try:
|
|
781
|
+
root_type = type_system.get_type(type_name)
|
|
782
|
+
root_types.append((type_name, root_type))
|
|
783
|
+
logger.info(f"Successfully loaded type: {type_name}")
|
|
784
|
+
except Exception as e:
|
|
785
|
+
logger.warning(f"Type '{type_name}' not found in type system. Skipping. Error: {e}")
|
|
786
|
+
|
|
787
|
+
if not root_types:
|
|
788
|
+
raise ValueError("None of the provided types could be found in the MCLI namespace")
|
|
789
|
+
|
|
790
|
+
# Process all types and their relationships
|
|
791
|
+
processed_types = set()
|
|
792
|
+
type_depth = {} # Maps type name to depth from any root
|
|
793
|
+
entities = {}
|
|
794
|
+
|
|
795
|
+
# Initialize processing queue with all root types at depth 0
|
|
796
|
+
to_process = [
|
|
797
|
+
(name, obj, 0) for name, obj in root_types
|
|
798
|
+
] # (type_name, type_obj, current_depth)
|
|
799
|
+
|
|
800
|
+
# Add all root types to the processed set
|
|
801
|
+
for name, _ in root_types:
|
|
802
|
+
processed_types.add(name)
|
|
803
|
+
type_depth[name] = 0 # Root types are at depth 0
|
|
804
|
+
ERD.add_entity(entities, name, type_system)
|
|
805
|
+
|
|
806
|
+
# Process all types up to max_depth
|
|
807
|
+
process_types_to_depth(
|
|
808
|
+
to_process, processed_types, type_depth, entities, type_system, max_depth
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
# Create a merged graph visualization
|
|
812
|
+
graph = create_merged_graph(entities, type_depth, root_types, include_methods)
|
|
813
|
+
|
|
814
|
+
# Save the graph to files
|
|
815
|
+
depth_info = f"_depth{max_depth}"
|
|
816
|
+
time_info = str(int(time.time() * 1000000))
|
|
817
|
+
dot_file = f"{output_prefix}{depth_info}_{time_info}.dot"
|
|
818
|
+
png_file = f"{output_prefix}{depth_info}_{time_info}.png"
|
|
819
|
+
|
|
820
|
+
graph.write_png(png_file)
|
|
821
|
+
graph.write_raw(dot_file)
|
|
822
|
+
|
|
823
|
+
logger.info(f"Merged ERD generated successfully. Output files: {png_file} and {dot_file}")
|
|
824
|
+
|
|
825
|
+
return dot_file, png_file
|
|
826
|
+
|
|
827
|
+
|
|
828
|
+
def process_types_to_depth(
|
|
829
|
+
to_process: List[Tuple[str, object, int]],
|
|
830
|
+
processed_types: Set[str],
|
|
831
|
+
type_depth: Dict[str, int],
|
|
832
|
+
entities: Dict[str, Dict],
|
|
833
|
+
type_system: TypeSystem,
|
|
834
|
+
max_depth: int,
|
|
835
|
+
) -> None:
|
|
836
|
+
"""
|
|
837
|
+
Process types recursively up to max_depth, building entity information.
|
|
838
|
+
|
|
839
|
+
Args:
|
|
840
|
+
to_process: Queue of types to process (type_name, type_obj, current_depth)
|
|
841
|
+
processed_types: Set of already processed type names
|
|
842
|
+
type_depth: Dictionary mapping type names to their depth
|
|
843
|
+
entities: Dictionary to store entity information
|
|
844
|
+
mcli: MCLI connection object
|
|
845
|
+
max_depth: Maximum depth to process
|
|
846
|
+
"""
|
|
847
|
+
while to_process:
|
|
848
|
+
current_type_name, current_type_obj, current_depth = to_process.pop(0)
|
|
849
|
+
|
|
850
|
+
if current_depth >= max_depth:
|
|
851
|
+
continue
|
|
852
|
+
|
|
853
|
+
# Get relevant types for the current type
|
|
854
|
+
try:
|
|
855
|
+
current_type_metadata = type_system.create_type_metadata(current_type_obj)
|
|
856
|
+
relevant_types = ERD.get_relevant_types(current_type_metadata)
|
|
857
|
+
|
|
858
|
+
for related_type_name in relevant_types:
|
|
859
|
+
# Check if we've already processed this type
|
|
860
|
+
if related_type_name in processed_types:
|
|
861
|
+
# Update depth if we found a shorter path
|
|
862
|
+
if current_depth + 1 < type_depth.get(related_type_name, float("inf")):
|
|
863
|
+
type_depth[related_type_name] = current_depth + 1
|
|
864
|
+
continue
|
|
865
|
+
|
|
866
|
+
try:
|
|
867
|
+
related_type_obj = type_system.get_type(related_type_name)
|
|
868
|
+
|
|
869
|
+
# Add entity information
|
|
870
|
+
ERD.add_entity(entities, related_type_name, type_system)
|
|
871
|
+
processed_types.add(related_type_name)
|
|
872
|
+
type_depth[related_type_name] = current_depth + 1
|
|
873
|
+
|
|
874
|
+
# Add to processing queue
|
|
875
|
+
to_process.append((related_type_name, related_type_obj, current_depth + 1))
|
|
876
|
+
except Exception as e:
|
|
877
|
+
logger.warning(f"Error loading related type {related_type_name}: {e}")
|
|
878
|
+
except Exception as e:
|
|
879
|
+
logger.warning(f"Error processing type {current_type_name}: {e}")
|
|
880
|
+
|
|
881
|
+
|
|
882
|
+
def create_merged_graph(
|
|
883
|
+
entities: Dict[str, Dict],
|
|
884
|
+
type_depth: Dict[str, int],
|
|
885
|
+
root_types: List[Tuple[str, object]],
|
|
886
|
+
include_methods: bool = False,
|
|
887
|
+
) -> pydot.Dot:
|
|
888
|
+
"""
|
|
889
|
+
Create a merged graph visualization from the processed entities.
|
|
890
|
+
|
|
891
|
+
Args:
|
|
892
|
+
entities: Dictionary of entity information
|
|
893
|
+
type_depth: Dictionary mapping type names to their depth
|
|
894
|
+
root_types: List of (type_name, type_obj) tuples representing root types
|
|
895
|
+
include_methods: Whether to include method information
|
|
896
|
+
|
|
897
|
+
Returns:
|
|
898
|
+
pydot.Dot graph object
|
|
899
|
+
"""
|
|
900
|
+
# Create a new graph
|
|
901
|
+
graph = pydot.Dot(
|
|
902
|
+
graph_type="digraph",
|
|
903
|
+
rankdir="TB",
|
|
904
|
+
splines="ortho",
|
|
905
|
+
bgcolor="white",
|
|
906
|
+
label="Merged Entity Relationship Diagram",
|
|
907
|
+
fontsize=16,
|
|
908
|
+
labelloc="t",
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
# Function to create table-based node labels
|
|
912
|
+
def create_table_html(entity, entity_data, font_size=10):
|
|
913
|
+
fields = entity_data["fields"]
|
|
914
|
+
methods = entity_data["methods"] if include_methods else {}
|
|
915
|
+
|
|
916
|
+
entity = entity.replace(".", "_")
|
|
917
|
+
entity = entity.replace("<", "[")
|
|
918
|
+
entity = entity.replace(">", "]")
|
|
919
|
+
|
|
920
|
+
html = f'<<TABLE BORDER="0" CELLBORDER="1" CELLSPACING="0" CELLPADDING="2">'
|
|
921
|
+
html += f'<TR><TD PORT="header" COLSPAN="2" BGCOLOR="lightgrey"><B><FONT POINT-SIZE="{font_size+2}">{entity}</FONT></B></TD></TR>'
|
|
922
|
+
|
|
923
|
+
# Fields/Members section
|
|
924
|
+
if fields:
|
|
925
|
+
html += f'<TR><TD COLSPAN="2" BGCOLOR="#E0E0E0"><B><FONT POINT-SIZE="{font_size}">Fields</FONT></B></TD></TR>'
|
|
926
|
+
for field, type_ in fields:
|
|
927
|
+
type_ = type_.replace("<", "[")
|
|
928
|
+
type_ = type_.replace(">", "]")
|
|
929
|
+
if not type_:
|
|
930
|
+
continue
|
|
931
|
+
html += f'<TR><TD><FONT POINT-SIZE="{font_size}">{field}</FONT></TD><TD><FONT POINT-SIZE="{font_size}">{type_}</FONT></TD></TR>'
|
|
932
|
+
|
|
933
|
+
# Methods section
|
|
934
|
+
if methods:
|
|
935
|
+
html += f'<TR><TD COLSPAN="2" BGCOLOR="#E0E0E0"><B><FONT POINT-SIZE="{font_size}">Methods</FONT></B></TD></TR>'
|
|
936
|
+
for method in methods:
|
|
937
|
+
html += f'<TR><TD COLSPAN="2"><FONT POINT-SIZE="{font_size}">{method}()</FONT></TD></TR>'
|
|
938
|
+
|
|
939
|
+
html += "</TABLE>>"
|
|
940
|
+
return html
|
|
941
|
+
|
|
942
|
+
# Create nodes for all entities
|
|
943
|
+
added_nodes = set() # Track added nodes to avoid duplicates
|
|
944
|
+
|
|
945
|
+
for entity, entity_data in entities.items():
|
|
946
|
+
entity_normalized = entity.replace(".", "_")
|
|
947
|
+
|
|
948
|
+
if entity_normalized in added_nodes:
|
|
949
|
+
continue
|
|
950
|
+
|
|
951
|
+
# Create a node with table-style label showing fields and methods
|
|
952
|
+
node_label = create_table_html(entity, entity_data, font_size=10)
|
|
953
|
+
|
|
954
|
+
# Determine node color based on depth
|
|
955
|
+
node_depth = type_depth.get(entity, 0)
|
|
956
|
+
is_root = any(entity == name for name, _ in root_types)
|
|
957
|
+
|
|
958
|
+
if is_root:
|
|
959
|
+
bg_color = "lightblue" # Root node
|
|
960
|
+
elif node_depth == 1:
|
|
961
|
+
bg_color = "#E6F5FF" # Light blue for first level
|
|
962
|
+
elif node_depth == 2:
|
|
963
|
+
bg_color = "#F0F8FF" # Even lighter blue for second level
|
|
964
|
+
else:
|
|
965
|
+
bg_color = "white" # Default for deeper levels
|
|
966
|
+
|
|
967
|
+
# Create the node
|
|
968
|
+
node = pydot.Node(
|
|
969
|
+
entity_normalized,
|
|
970
|
+
shape="none", # Using 'none' to allow custom HTML table
|
|
971
|
+
label=node_label,
|
|
972
|
+
style="filled",
|
|
973
|
+
fillcolor=bg_color,
|
|
974
|
+
margin="0",
|
|
975
|
+
)
|
|
976
|
+
graph.add_node(node)
|
|
977
|
+
added_nodes.add(entity_normalized)
|
|
978
|
+
|
|
979
|
+
# Build relationship map
|
|
980
|
+
relationship_map = build_relationship_map(entities)
|
|
981
|
+
|
|
982
|
+
# Add edges based on relationships
|
|
983
|
+
for source, targets in relationship_map.items():
|
|
984
|
+
source_normalized = source.replace(".", "_")
|
|
985
|
+
for target in targets:
|
|
986
|
+
target_normalized = target.replace(".", "_")
|
|
987
|
+
|
|
988
|
+
# Skip if either node wasn't added
|
|
989
|
+
if source_normalized not in added_nodes or target_normalized not in added_nodes:
|
|
990
|
+
continue
|
|
991
|
+
|
|
992
|
+
edge = pydot.Edge(
|
|
993
|
+
source_normalized,
|
|
994
|
+
target_normalized,
|
|
995
|
+
dir="both",
|
|
996
|
+
arrowtail="none",
|
|
997
|
+
arrowhead="normal",
|
|
998
|
+
constraint=True,
|
|
999
|
+
color="black",
|
|
1000
|
+
penwidth=1.5,
|
|
1001
|
+
)
|
|
1002
|
+
graph.add_edge(edge)
|
|
1003
|
+
|
|
1004
|
+
# Create a subgraph to force root nodes to be at the top
|
|
1005
|
+
root_subgraph = pydot.Subgraph(rank="min")
|
|
1006
|
+
for name, _ in root_types:
|
|
1007
|
+
normalized_name = name.replace(".", "_")
|
|
1008
|
+
if normalized_name in added_nodes:
|
|
1009
|
+
root_subgraph.add_node(pydot.Node(normalized_name))
|
|
1010
|
+
graph.add_subgraph(root_subgraph)
|
|
1011
|
+
|
|
1012
|
+
return graph
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
def build_relationship_map(entities: Dict[str, Dict]) -> Dict[str, Set[str]]:
|
|
1016
|
+
"""
|
|
1017
|
+
Build a map of relationships between entities based on field types.
|
|
1018
|
+
|
|
1019
|
+
Args:
|
|
1020
|
+
entities: Dictionary of entity information
|
|
1021
|
+
|
|
1022
|
+
Returns:
|
|
1023
|
+
Dictionary mapping source entity names to sets of target entity names
|
|
1024
|
+
"""
|
|
1025
|
+
relationship_map = {}
|
|
1026
|
+
|
|
1027
|
+
for entity_name, entity_data in entities.items():
|
|
1028
|
+
fields = entity_data["fields"]
|
|
1029
|
+
|
|
1030
|
+
for field_name, field_type in fields:
|
|
1031
|
+
# Skip empty field types
|
|
1032
|
+
if not field_type:
|
|
1033
|
+
continue
|
|
1034
|
+
|
|
1035
|
+
# Handle array types like [Type]
|
|
1036
|
+
is_array = field_type.startswith("[") and field_type.endswith("]")
|
|
1037
|
+
if is_array:
|
|
1038
|
+
target_type = field_type[1:-1] # Remove brackets
|
|
1039
|
+
else:
|
|
1040
|
+
target_type = field_type
|
|
1041
|
+
|
|
1042
|
+
# Skip primitive types
|
|
1043
|
+
if target_type in [
|
|
1044
|
+
"StringType",
|
|
1045
|
+
"IntegerType",
|
|
1046
|
+
"DoubleType",
|
|
1047
|
+
"DateTimeType",
|
|
1048
|
+
"BooleanType",
|
|
1049
|
+
]:
|
|
1050
|
+
continue
|
|
1051
|
+
|
|
1052
|
+
# Add relationship
|
|
1053
|
+
if entity_name not in relationship_map:
|
|
1054
|
+
relationship_map[entity_name] = set()
|
|
1055
|
+
relationship_map[entity_name].add(target_type)
|
|
1056
|
+
|
|
1057
|
+
return relationship_map
|
|
1058
|
+
|
|
1059
|
+
|
|
1060
|
+
def generate_merged_erd_for_types(
|
|
1061
|
+
type_names: List[str],
|
|
1062
|
+
env_url: str = None,
|
|
1063
|
+
type_system: Optional[TypeSystem] = None,
|
|
1064
|
+
max_depth: int = 2,
|
|
1065
|
+
output_prefix: str = "MergedERD",
|
|
1066
|
+
) -> Tuple[str, str]:
|
|
1067
|
+
"""
|
|
1068
|
+
Generate a merged ERD for multiple MCLI types.
|
|
1069
|
+
|
|
1070
|
+
Args:
|
|
1071
|
+
type_names: List of MCLI type names to include as root nodes
|
|
1072
|
+
env_url: Environment URL to connect to MCLI
|
|
1073
|
+
max_depth: Maximum depth of relationships to include in the diagram
|
|
1074
|
+
output_prefix: Prefix for output files
|
|
1075
|
+
|
|
1076
|
+
Returns:
|
|
1077
|
+
Tuple of (dot_file_path, png_file_path)
|
|
1078
|
+
"""
|
|
1079
|
+
return create_merged_erd(
|
|
1080
|
+
types=type_names,
|
|
1081
|
+
type_system=type_system,
|
|
1082
|
+
env_url=env_url,
|
|
1083
|
+
max_depth=max_depth,
|
|
1084
|
+
output_prefix=output_prefix,
|
|
1085
|
+
)
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
def find_top_nodes_in_graph(graph_data: Dict, top_n: int = 5) -> List[Tuple[str, int]]:
|
|
1089
|
+
"""
|
|
1090
|
+
Find the top N nodes in a graph that would serve as good roots for a hierarchical export.
|
|
1091
|
+
Nodes are ranked by the number of descendants they have (size of reachable subgraph).
|
|
1092
|
+
|
|
1093
|
+
Args:
|
|
1094
|
+
graph_data: Dictionary containing graph data with vertices and edges
|
|
1095
|
+
top_n: Number of top-level nodes to return
|
|
1096
|
+
|
|
1097
|
+
Returns:
|
|
1098
|
+
List of tuples containing (node_id, descendant_count) sorted by descendant count
|
|
1099
|
+
"""
|
|
1100
|
+
logger.info("START | find_top_nodes_in_graph")
|
|
1101
|
+
try:
|
|
1102
|
+
# Ensure top_n is an integer
|
|
1103
|
+
try:
|
|
1104
|
+
top_n = int(top_n)
|
|
1105
|
+
except (ValueError, TypeError):
|
|
1106
|
+
logger.warning(f"Invalid top_n value: {top_n}, using default of 5")
|
|
1107
|
+
top_n = 5
|
|
1108
|
+
|
|
1109
|
+
# Build adjacency list from the graph data
|
|
1110
|
+
from mcli.app.main.generate_graph import build_adjacency_list, count_descendants
|
|
1111
|
+
|
|
1112
|
+
logger.info("START INVOKE | build_adjacency_list")
|
|
1113
|
+
node_map, adj_list = build_adjacency_list(graph_data)
|
|
1114
|
+
logger.info("END INVOKE | build_adjacency_list")
|
|
1115
|
+
|
|
1116
|
+
# Count descendants for each node
|
|
1117
|
+
descendant_counts = {}
|
|
1118
|
+
logger.info("START INVOKE | count_descendants")
|
|
1119
|
+
for node_id in node_map:
|
|
1120
|
+
descendant_counts[node_id] = count_descendants(node_id, adj_list)
|
|
1121
|
+
logger.info("END INVOKE | count_descendants")
|
|
1122
|
+
|
|
1123
|
+
# Sort nodes by descendant count
|
|
1124
|
+
logger.info("START INVOKE | descendant_counts.items()")
|
|
1125
|
+
sorted_nodes = sorted(descendant_counts.items(), key=lambda x: x[1], reverse=True)
|
|
1126
|
+
logger.info("END INVOKE | descendant_counts.items()")
|
|
1127
|
+
|
|
1128
|
+
# Return top N nodes
|
|
1129
|
+
logger.info(f"START INVOKE [(node_id, count) for node_id, count in sorted_nodes[:{top_n}]]")
|
|
1130
|
+
top_nodes = [(node_id, count) for node_id, count in sorted_nodes[:top_n]]
|
|
1131
|
+
logger.info(f"END INVOKE [(node_id, count) for node_id, count in sorted_nodes[:{top_n}]]")
|
|
1132
|
+
logger.info("END | find_top_nodes_in_graph")
|
|
1133
|
+
return top_nodes
|
|
1134
|
+
except Exception as e:
|
|
1135
|
+
logger.error(f"Error finding top nodes in graph: {e}")
|
|
1136
|
+
return []
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
def generate_erd_for_top_nodes(
|
|
1140
|
+
graph_file_path: str, max_depth: int = 2, top_n: int = 50
|
|
1141
|
+
) -> List[Tuple[str, str, int]]:
|
|
1142
|
+
"""
|
|
1143
|
+
Generate ERDs for the top N nodes in a graph.
|
|
1144
|
+
|
|
1145
|
+
Args:
|
|
1146
|
+
graph_file_path: Path to the JSON file containing the graph data
|
|
1147
|
+
max_depth: Maximum depth for building the hierarchical model
|
|
1148
|
+
top_n: Number of top-level nodes to include
|
|
1149
|
+
|
|
1150
|
+
Returns:
|
|
1151
|
+
List of tuples containing (dot_file_path, png_file_path, descendant_count) for each top node
|
|
1152
|
+
"""
|
|
1153
|
+
logger.info("START | generate_erd_for_top_nodes")
|
|
1154
|
+
try:
|
|
1155
|
+
# Check if the file exists
|
|
1156
|
+
if not os.path.exists(graph_file_path):
|
|
1157
|
+
logger.error(f"Graph file not found: {graph_file_path}")
|
|
1158
|
+
raise FileNotFoundError(f"Graph file not found: {graph_file_path}")
|
|
1159
|
+
|
|
1160
|
+
# Load the graph data
|
|
1161
|
+
try:
|
|
1162
|
+
with open(graph_file_path, "r") as f:
|
|
1163
|
+
graph_data = json.load(f)
|
|
1164
|
+
except json.JSONDecodeError as e:
|
|
1165
|
+
logger.error(f"Invalid JSON in graph file: {e}")
|
|
1166
|
+
raise ValueError(f"Invalid JSON in graph file: {e}")
|
|
1167
|
+
|
|
1168
|
+
# Find top nodes
|
|
1169
|
+
top_nodes = find_top_nodes_in_graph(graph_data, top_n)
|
|
1170
|
+
if not top_nodes:
|
|
1171
|
+
logger.warning("No top nodes found in the graph.")
|
|
1172
|
+
return []
|
|
1173
|
+
|
|
1174
|
+
logger.info(
|
|
1175
|
+
f"Found {len(top_nodes)} top nodes in the graph: {', '.join([node_id for node_id, _ in top_nodes])}"
|
|
1176
|
+
)
|
|
1177
|
+
|
|
1178
|
+
# Generate ERDs for each top node
|
|
1179
|
+
from mcli.app.main.generate_graph import (
|
|
1180
|
+
build_adjacency_list,
|
|
1181
|
+
build_hierarchical_graph,
|
|
1182
|
+
create_dot_graph,
|
|
1183
|
+
)
|
|
1184
|
+
|
|
1185
|
+
# Build adjacency list from the graph data
|
|
1186
|
+
logger.info("START | build_adjacency_list")
|
|
1187
|
+
node_map, adj_list = build_adjacency_list(graph_data)
|
|
1188
|
+
logger.info("END | build_adjacency_list")
|
|
1189
|
+
|
|
1190
|
+
# Build hierarchical graph with top nodes as roots
|
|
1191
|
+
top_node_ids = [node_id for node_id, _ in top_nodes]
|
|
1192
|
+
logger.info(
|
|
1193
|
+
f"Building hierarchical graph with {len(top_node_ids)} root nodes and max depth {max_depth}"
|
|
1194
|
+
)
|
|
1195
|
+
logger.info("START | build_hierarchical_graph")
|
|
1196
|
+
hierarchy = build_hierarchical_graph(top_node_ids, node_map, adj_list, max_depth)
|
|
1197
|
+
logger.info("END | build_hierarchical_graph")
|
|
1198
|
+
|
|
1199
|
+
# Create output directory if it doesn't exist
|
|
1200
|
+
output_dir = os.path.join(os.path.dirname(os.path.abspath(graph_file_path)), "output")
|
|
1201
|
+
if not os.path.exists(output_dir):
|
|
1202
|
+
os.makedirs(output_dir)
|
|
1203
|
+
logger.info(f"Created output directory: {output_dir}")
|
|
1204
|
+
|
|
1205
|
+
# Generate file paths
|
|
1206
|
+
timestamp = str(int(time.time() * 1000000))
|
|
1207
|
+
generated_files = []
|
|
1208
|
+
|
|
1209
|
+
# For each top-level node, generate DOT and PNG files
|
|
1210
|
+
for root_node_id, descendant_count in top_nodes:
|
|
1211
|
+
try:
|
|
1212
|
+
# Create the DOT graph
|
|
1213
|
+
logger.info(f"Creating DOT graph for {root_node_id}")
|
|
1214
|
+
dot_graph = create_dot_graph(hierarchy, root_node_id, max_depth)
|
|
1215
|
+
|
|
1216
|
+
# Define file paths
|
|
1217
|
+
depth_info = f"_depth{max_depth}"
|
|
1218
|
+
dot_file = os.path.join(output_dir, f"{root_node_id}{depth_info}_{timestamp}.dot")
|
|
1219
|
+
png_file = os.path.join(output_dir, f"{root_node_id}{depth_info}_{timestamp}.png")
|
|
1220
|
+
|
|
1221
|
+
# Save the files
|
|
1222
|
+
logger.info(f"Saving DOT file to {dot_file}")
|
|
1223
|
+
dot_graph.write_raw(dot_file)
|
|
1224
|
+
|
|
1225
|
+
logger.info(f"Generating PNG file to {png_file}")
|
|
1226
|
+
dot_graph.write_png(png_file)
|
|
1227
|
+
|
|
1228
|
+
# Return just the filenames (not full paths) for consistency
|
|
1229
|
+
dot_filename = os.path.basename(dot_file)
|
|
1230
|
+
png_filename = os.path.basename(png_file)
|
|
1231
|
+
|
|
1232
|
+
generated_files.append((dot_filename, png_filename, descendant_count))
|
|
1233
|
+
logger.info(
|
|
1234
|
+
f"Generated graph for {root_node_id} with {descendant_count} descendants: {png_filename}"
|
|
1235
|
+
)
|
|
1236
|
+
except Exception as e:
|
|
1237
|
+
logger.error(f"Error generating graph for node {root_node_id}: {e}")
|
|
1238
|
+
|
|
1239
|
+
if not generated_files:
|
|
1240
|
+
logger.warning("No files were generated successfully")
|
|
1241
|
+
|
|
1242
|
+
return generated_files
|
|
1243
|
+
except Exception as e:
|
|
1244
|
+
logger.error(f"Error generating ERDs for top nodes: {e}")
|
|
1245
|
+
import traceback
|
|
1246
|
+
|
|
1247
|
+
logger.error(f"Traceback: {traceback.format_exc()}")
|
|
1248
|
+
raise
|
|
1249
|
+
|
|
1250
|
+
|
|
1251
|
+
def analyze_graph_for_hierarchical_exports(graph_file_path: str, top_n: int = 5) -> List[Dict]:
|
|
1252
|
+
"""
|
|
1253
|
+
Analyze a graph to identify the top N nodes that would serve as good roots for
|
|
1254
|
+
hierarchical exports, based on the number of descendants each node has.
|
|
1255
|
+
|
|
1256
|
+
Args:
|
|
1257
|
+
graph_file_path: Path to the JSON file containing the graph data
|
|
1258
|
+
top_n: Number of top-level nodes to identify
|
|
1259
|
+
|
|
1260
|
+
Returns:
|
|
1261
|
+
List of dictionaries containing information about each top node
|
|
1262
|
+
"""
|
|
1263
|
+
logger.info("analyze_graph_for_hierarchical_exports")
|
|
1264
|
+
try:
|
|
1265
|
+
logger.info(f"Analyzing graph file: {graph_file_path}")
|
|
1266
|
+
|
|
1267
|
+
# Check if the file exists
|
|
1268
|
+
if not os.path.exists(graph_file_path):
|
|
1269
|
+
logger.error(f"Graph file not found: {graph_file_path}")
|
|
1270
|
+
raise FileNotFoundError(f"Graph file not found: {graph_file_path}")
|
|
1271
|
+
|
|
1272
|
+
# Load the graph data
|
|
1273
|
+
try:
|
|
1274
|
+
with open(graph_file_path, "r") as f:
|
|
1275
|
+
graph_data = json.load(f)
|
|
1276
|
+
except json.JSONDecodeError as e:
|
|
1277
|
+
logger.error(f"Invalid JSON in graph file: {e}")
|
|
1278
|
+
raise ValueError(f"Invalid JSON in graph file: {e}")
|
|
1279
|
+
|
|
1280
|
+
# Validate the structure of the graph data
|
|
1281
|
+
if "graph" not in graph_data:
|
|
1282
|
+
logger.error("Invalid graph data: missing 'graph' key")
|
|
1283
|
+
raise ValueError("Invalid graph data: missing 'graph' key")
|
|
1284
|
+
|
|
1285
|
+
# Find top nodes
|
|
1286
|
+
logger.info(f"Finding top {top_n} nodes in graph by descendant count")
|
|
1287
|
+
top_nodes = find_top_nodes_in_graph(graph_data, top_n)
|
|
1288
|
+
if not top_nodes:
|
|
1289
|
+
logger.warning("No top nodes found in the graph.")
|
|
1290
|
+
return []
|
|
1291
|
+
|
|
1292
|
+
logger.info(f"Found {len(top_nodes)} top nodes")
|
|
1293
|
+
|
|
1294
|
+
# Build adjacency list from the graph data
|
|
1295
|
+
from mcli.app.main.generate_graph import build_adjacency_list
|
|
1296
|
+
|
|
1297
|
+
logger.info("Building adjacency list from graph data")
|
|
1298
|
+
node_map, adj_list = build_adjacency_list(graph_data)
|
|
1299
|
+
|
|
1300
|
+
# Get node information
|
|
1301
|
+
results = []
|
|
1302
|
+
for node_id, descendant_count in top_nodes:
|
|
1303
|
+
try:
|
|
1304
|
+
node_info = node_map.get(node_id, {})
|
|
1305
|
+
|
|
1306
|
+
# Extract node metadata
|
|
1307
|
+
node_type = node_info.get("type", "Unknown")
|
|
1308
|
+
node_category = node_info.get("category", "Unknown")
|
|
1309
|
+
|
|
1310
|
+
# Extract additional data if available
|
|
1311
|
+
data = node_info.get("data", {})
|
|
1312
|
+
name = data.get("name", node_id)
|
|
1313
|
+
package = data.get("package", "Unknown")
|
|
1314
|
+
|
|
1315
|
+
# Get direct children
|
|
1316
|
+
direct_children = adj_list.get(node_id, [])
|
|
1317
|
+
|
|
1318
|
+
results.append(
|
|
1319
|
+
{
|
|
1320
|
+
"id": node_id,
|
|
1321
|
+
"name": name,
|
|
1322
|
+
"type": node_type,
|
|
1323
|
+
"category": node_category,
|
|
1324
|
+
"package": package,
|
|
1325
|
+
"descendant_count": descendant_count,
|
|
1326
|
+
"direct_children_count": len(direct_children),
|
|
1327
|
+
"direct_children": direct_children[
|
|
1328
|
+
:10
|
|
1329
|
+
], # Limit to first 10 children for brevity
|
|
1330
|
+
}
|
|
1331
|
+
)
|
|
1332
|
+
|
|
1333
|
+
logger.info(
|
|
1334
|
+
f"Top node: {node_id} - {name} - {descendant_count} descendants, {len(direct_children)} direct children"
|
|
1335
|
+
)
|
|
1336
|
+
except Exception as e:
|
|
1337
|
+
logger.error(f"Error processing node {node_id}: {e}")
|
|
1338
|
+
|
|
1339
|
+
return results
|
|
1340
|
+
except Exception as e:
|
|
1341
|
+
logger.error(f"Error analyzing graph for hierarchical exports: {e}")
|
|
1342
|
+
import traceback
|
|
1343
|
+
|
|
1344
|
+
logger.error(f"Traceback: {traceback.format_exc()}")
|
|
1345
|
+
raise
|