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.

Files changed (186) hide show
  1. mcli/app/chat_cmd.py +42 -0
  2. mcli/app/commands_cmd.py +226 -0
  3. mcli/app/completion_cmd.py +216 -0
  4. mcli/app/completion_helpers.py +288 -0
  5. mcli/app/cron_test_cmd.py +697 -0
  6. mcli/app/logs_cmd.py +419 -0
  7. mcli/app/main.py +492 -0
  8. mcli/app/model/model.py +1060 -0
  9. mcli/app/model_cmd.py +227 -0
  10. mcli/app/redis_cmd.py +269 -0
  11. mcli/app/video/video.py +1114 -0
  12. mcli/app/visual_cmd.py +303 -0
  13. mcli/chat/chat.py +2409 -0
  14. mcli/chat/command_rag.py +514 -0
  15. mcli/chat/enhanced_chat.py +652 -0
  16. mcli/chat/system_controller.py +1010 -0
  17. mcli/chat/system_integration.py +1016 -0
  18. mcli/cli.py +25 -0
  19. mcli/config.toml +20 -0
  20. mcli/lib/api/api.py +586 -0
  21. mcli/lib/api/daemon_client.py +203 -0
  22. mcli/lib/api/daemon_client_local.py +44 -0
  23. mcli/lib/api/daemon_decorator.py +217 -0
  24. mcli/lib/api/mcli_decorators.py +1032 -0
  25. mcli/lib/auth/auth.py +85 -0
  26. mcli/lib/auth/aws_manager.py +85 -0
  27. mcli/lib/auth/azure_manager.py +91 -0
  28. mcli/lib/auth/credential_manager.py +192 -0
  29. mcli/lib/auth/gcp_manager.py +93 -0
  30. mcli/lib/auth/key_manager.py +117 -0
  31. mcli/lib/auth/mcli_manager.py +93 -0
  32. mcli/lib/auth/token_manager.py +75 -0
  33. mcli/lib/auth/token_util.py +1011 -0
  34. mcli/lib/config/config.py +47 -0
  35. mcli/lib/discovery/__init__.py +1 -0
  36. mcli/lib/discovery/command_discovery.py +274 -0
  37. mcli/lib/erd/erd.py +1345 -0
  38. mcli/lib/erd/generate_graph.py +453 -0
  39. mcli/lib/files/files.py +76 -0
  40. mcli/lib/fs/fs.py +109 -0
  41. mcli/lib/lib.py +29 -0
  42. mcli/lib/logger/logger.py +611 -0
  43. mcli/lib/performance/optimizer.py +409 -0
  44. mcli/lib/performance/rust_bridge.py +502 -0
  45. mcli/lib/performance/uvloop_config.py +154 -0
  46. mcli/lib/pickles/pickles.py +50 -0
  47. mcli/lib/search/cached_vectorizer.py +479 -0
  48. mcli/lib/services/data_pipeline.py +460 -0
  49. mcli/lib/services/lsh_client.py +441 -0
  50. mcli/lib/services/redis_service.py +387 -0
  51. mcli/lib/shell/shell.py +137 -0
  52. mcli/lib/toml/toml.py +33 -0
  53. mcli/lib/ui/styling.py +47 -0
  54. mcli/lib/ui/visual_effects.py +634 -0
  55. mcli/lib/watcher/watcher.py +185 -0
  56. mcli/ml/api/app.py +215 -0
  57. mcli/ml/api/middleware.py +224 -0
  58. mcli/ml/api/routers/admin_router.py +12 -0
  59. mcli/ml/api/routers/auth_router.py +244 -0
  60. mcli/ml/api/routers/backtest_router.py +12 -0
  61. mcli/ml/api/routers/data_router.py +12 -0
  62. mcli/ml/api/routers/model_router.py +302 -0
  63. mcli/ml/api/routers/monitoring_router.py +12 -0
  64. mcli/ml/api/routers/portfolio_router.py +12 -0
  65. mcli/ml/api/routers/prediction_router.py +267 -0
  66. mcli/ml/api/routers/trade_router.py +12 -0
  67. mcli/ml/api/routers/websocket_router.py +76 -0
  68. mcli/ml/api/schemas.py +64 -0
  69. mcli/ml/auth/auth_manager.py +425 -0
  70. mcli/ml/auth/models.py +154 -0
  71. mcli/ml/auth/permissions.py +302 -0
  72. mcli/ml/backtesting/backtest_engine.py +502 -0
  73. mcli/ml/backtesting/performance_metrics.py +393 -0
  74. mcli/ml/cache.py +400 -0
  75. mcli/ml/cli/main.py +398 -0
  76. mcli/ml/config/settings.py +394 -0
  77. mcli/ml/configs/dvc_config.py +230 -0
  78. mcli/ml/configs/mlflow_config.py +131 -0
  79. mcli/ml/configs/mlops_manager.py +293 -0
  80. mcli/ml/dashboard/app.py +532 -0
  81. mcli/ml/dashboard/app_integrated.py +738 -0
  82. mcli/ml/dashboard/app_supabase.py +560 -0
  83. mcli/ml/dashboard/app_training.py +615 -0
  84. mcli/ml/dashboard/cli.py +51 -0
  85. mcli/ml/data_ingestion/api_connectors.py +501 -0
  86. mcli/ml/data_ingestion/data_pipeline.py +567 -0
  87. mcli/ml/data_ingestion/stream_processor.py +512 -0
  88. mcli/ml/database/migrations/env.py +94 -0
  89. mcli/ml/database/models.py +667 -0
  90. mcli/ml/database/session.py +200 -0
  91. mcli/ml/experimentation/ab_testing.py +845 -0
  92. mcli/ml/features/ensemble_features.py +607 -0
  93. mcli/ml/features/political_features.py +676 -0
  94. mcli/ml/features/recommendation_engine.py +809 -0
  95. mcli/ml/features/stock_features.py +573 -0
  96. mcli/ml/features/test_feature_engineering.py +346 -0
  97. mcli/ml/logging.py +85 -0
  98. mcli/ml/mlops/data_versioning.py +518 -0
  99. mcli/ml/mlops/experiment_tracker.py +377 -0
  100. mcli/ml/mlops/model_serving.py +481 -0
  101. mcli/ml/mlops/pipeline_orchestrator.py +614 -0
  102. mcli/ml/models/base_models.py +324 -0
  103. mcli/ml/models/ensemble_models.py +675 -0
  104. mcli/ml/models/recommendation_models.py +474 -0
  105. mcli/ml/models/test_models.py +487 -0
  106. mcli/ml/monitoring/drift_detection.py +676 -0
  107. mcli/ml/monitoring/metrics.py +45 -0
  108. mcli/ml/optimization/portfolio_optimizer.py +834 -0
  109. mcli/ml/preprocessing/data_cleaners.py +451 -0
  110. mcli/ml/preprocessing/feature_extractors.py +491 -0
  111. mcli/ml/preprocessing/ml_pipeline.py +382 -0
  112. mcli/ml/preprocessing/politician_trading_preprocessor.py +569 -0
  113. mcli/ml/preprocessing/test_preprocessing.py +294 -0
  114. mcli/ml/scripts/populate_sample_data.py +200 -0
  115. mcli/ml/tasks.py +400 -0
  116. mcli/ml/tests/test_integration.py +429 -0
  117. mcli/ml/tests/test_training_dashboard.py +387 -0
  118. mcli/public/oi/oi.py +15 -0
  119. mcli/public/public.py +4 -0
  120. mcli/self/self_cmd.py +1246 -0
  121. mcli/workflow/daemon/api_daemon.py +800 -0
  122. mcli/workflow/daemon/async_command_database.py +681 -0
  123. mcli/workflow/daemon/async_process_manager.py +591 -0
  124. mcli/workflow/daemon/client.py +530 -0
  125. mcli/workflow/daemon/commands.py +1196 -0
  126. mcli/workflow/daemon/daemon.py +905 -0
  127. mcli/workflow/daemon/daemon_api.py +59 -0
  128. mcli/workflow/daemon/enhanced_daemon.py +571 -0
  129. mcli/workflow/daemon/process_cli.py +244 -0
  130. mcli/workflow/daemon/process_manager.py +439 -0
  131. mcli/workflow/daemon/test_daemon.py +275 -0
  132. mcli/workflow/dashboard/dashboard_cmd.py +113 -0
  133. mcli/workflow/docker/docker.py +0 -0
  134. mcli/workflow/file/file.py +100 -0
  135. mcli/workflow/gcloud/config.toml +21 -0
  136. mcli/workflow/gcloud/gcloud.py +58 -0
  137. mcli/workflow/git_commit/ai_service.py +328 -0
  138. mcli/workflow/git_commit/commands.py +430 -0
  139. mcli/workflow/lsh_integration.py +355 -0
  140. mcli/workflow/model_service/client.py +594 -0
  141. mcli/workflow/model_service/download_and_run_efficient_models.py +288 -0
  142. mcli/workflow/model_service/lightweight_embedder.py +397 -0
  143. mcli/workflow/model_service/lightweight_model_server.py +714 -0
  144. mcli/workflow/model_service/lightweight_test.py +241 -0
  145. mcli/workflow/model_service/model_service.py +1955 -0
  146. mcli/workflow/model_service/ollama_efficient_runner.py +425 -0
  147. mcli/workflow/model_service/pdf_processor.py +386 -0
  148. mcli/workflow/model_service/test_efficient_runner.py +234 -0
  149. mcli/workflow/model_service/test_example.py +315 -0
  150. mcli/workflow/model_service/test_integration.py +131 -0
  151. mcli/workflow/model_service/test_new_features.py +149 -0
  152. mcli/workflow/openai/openai.py +99 -0
  153. mcli/workflow/politician_trading/commands.py +1790 -0
  154. mcli/workflow/politician_trading/config.py +134 -0
  155. mcli/workflow/politician_trading/connectivity.py +490 -0
  156. mcli/workflow/politician_trading/data_sources.py +395 -0
  157. mcli/workflow/politician_trading/database.py +410 -0
  158. mcli/workflow/politician_trading/demo.py +248 -0
  159. mcli/workflow/politician_trading/models.py +165 -0
  160. mcli/workflow/politician_trading/monitoring.py +413 -0
  161. mcli/workflow/politician_trading/scrapers.py +966 -0
  162. mcli/workflow/politician_trading/scrapers_california.py +412 -0
  163. mcli/workflow/politician_trading/scrapers_eu.py +377 -0
  164. mcli/workflow/politician_trading/scrapers_uk.py +350 -0
  165. mcli/workflow/politician_trading/scrapers_us_states.py +438 -0
  166. mcli/workflow/politician_trading/supabase_functions.py +354 -0
  167. mcli/workflow/politician_trading/workflow.py +852 -0
  168. mcli/workflow/registry/registry.py +180 -0
  169. mcli/workflow/repo/repo.py +223 -0
  170. mcli/workflow/scheduler/commands.py +493 -0
  171. mcli/workflow/scheduler/cron_parser.py +238 -0
  172. mcli/workflow/scheduler/job.py +182 -0
  173. mcli/workflow/scheduler/monitor.py +139 -0
  174. mcli/workflow/scheduler/persistence.py +324 -0
  175. mcli/workflow/scheduler/scheduler.py +679 -0
  176. mcli/workflow/sync/sync_cmd.py +437 -0
  177. mcli/workflow/sync/test_cmd.py +314 -0
  178. mcli/workflow/videos/videos.py +242 -0
  179. mcli/workflow/wakatime/wakatime.py +11 -0
  180. mcli/workflow/workflow.py +37 -0
  181. mcli_framework-7.0.0.dist-info/METADATA +479 -0
  182. mcli_framework-7.0.0.dist-info/RECORD +186 -0
  183. mcli_framework-7.0.0.dist-info/WHEEL +5 -0
  184. mcli_framework-7.0.0.dist-info/entry_points.txt +7 -0
  185. mcli_framework-7.0.0.dist-info/licenses/LICENSE +21 -0
  186. 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