mainsequence 2.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.
Files changed (110) hide show
  1. mainsequence/__init__.py +0 -0
  2. mainsequence/__main__.py +9 -0
  3. mainsequence/cli/__init__.py +1 -0
  4. mainsequence/cli/api.py +157 -0
  5. mainsequence/cli/cli.py +442 -0
  6. mainsequence/cli/config.py +78 -0
  7. mainsequence/cli/ssh_utils.py +126 -0
  8. mainsequence/client/__init__.py +17 -0
  9. mainsequence/client/base.py +431 -0
  10. mainsequence/client/data_sources_interfaces/__init__.py +0 -0
  11. mainsequence/client/data_sources_interfaces/duckdb.py +1468 -0
  12. mainsequence/client/data_sources_interfaces/timescale.py +479 -0
  13. mainsequence/client/models_helpers.py +113 -0
  14. mainsequence/client/models_report_studio.py +412 -0
  15. mainsequence/client/models_tdag.py +2276 -0
  16. mainsequence/client/models_vam.py +1983 -0
  17. mainsequence/client/utils.py +387 -0
  18. mainsequence/dashboards/__init__.py +0 -0
  19. mainsequence/dashboards/streamlit/__init__.py +0 -0
  20. mainsequence/dashboards/streamlit/assets/config.toml +12 -0
  21. mainsequence/dashboards/streamlit/assets/favicon.png +0 -0
  22. mainsequence/dashboards/streamlit/assets/logo.png +0 -0
  23. mainsequence/dashboards/streamlit/core/__init__.py +0 -0
  24. mainsequence/dashboards/streamlit/core/theme.py +212 -0
  25. mainsequence/dashboards/streamlit/pages/__init__.py +0 -0
  26. mainsequence/dashboards/streamlit/scaffold.py +220 -0
  27. mainsequence/instrumentation/__init__.py +7 -0
  28. mainsequence/instrumentation/utils.py +101 -0
  29. mainsequence/instruments/__init__.py +1 -0
  30. mainsequence/instruments/data_interface/__init__.py +10 -0
  31. mainsequence/instruments/data_interface/data_interface.py +361 -0
  32. mainsequence/instruments/instruments/__init__.py +3 -0
  33. mainsequence/instruments/instruments/base_instrument.py +85 -0
  34. mainsequence/instruments/instruments/bond.py +447 -0
  35. mainsequence/instruments/instruments/european_option.py +74 -0
  36. mainsequence/instruments/instruments/interest_rate_swap.py +217 -0
  37. mainsequence/instruments/instruments/json_codec.py +585 -0
  38. mainsequence/instruments/instruments/knockout_fx_option.py +146 -0
  39. mainsequence/instruments/instruments/position.py +475 -0
  40. mainsequence/instruments/instruments/ql_fields.py +239 -0
  41. mainsequence/instruments/instruments/vanilla_fx_option.py +107 -0
  42. mainsequence/instruments/pricing_models/__init__.py +0 -0
  43. mainsequence/instruments/pricing_models/black_scholes.py +49 -0
  44. mainsequence/instruments/pricing_models/bond_pricer.py +182 -0
  45. mainsequence/instruments/pricing_models/fx_option_pricer.py +90 -0
  46. mainsequence/instruments/pricing_models/indices.py +350 -0
  47. mainsequence/instruments/pricing_models/knockout_fx_pricer.py +209 -0
  48. mainsequence/instruments/pricing_models/swap_pricer.py +502 -0
  49. mainsequence/instruments/settings.py +175 -0
  50. mainsequence/instruments/utils.py +29 -0
  51. mainsequence/logconf.py +284 -0
  52. mainsequence/reportbuilder/__init__.py +0 -0
  53. mainsequence/reportbuilder/__main__.py +0 -0
  54. mainsequence/reportbuilder/examples/ms_template_report.py +706 -0
  55. mainsequence/reportbuilder/model.py +713 -0
  56. mainsequence/reportbuilder/slide_templates.py +532 -0
  57. mainsequence/tdag/__init__.py +8 -0
  58. mainsequence/tdag/__main__.py +0 -0
  59. mainsequence/tdag/config.py +129 -0
  60. mainsequence/tdag/data_nodes/__init__.py +12 -0
  61. mainsequence/tdag/data_nodes/build_operations.py +751 -0
  62. mainsequence/tdag/data_nodes/data_nodes.py +1292 -0
  63. mainsequence/tdag/data_nodes/persist_managers.py +812 -0
  64. mainsequence/tdag/data_nodes/run_operations.py +543 -0
  65. mainsequence/tdag/data_nodes/utils.py +24 -0
  66. mainsequence/tdag/future_registry.py +25 -0
  67. mainsequence/tdag/utils.py +40 -0
  68. mainsequence/virtualfundbuilder/__init__.py +45 -0
  69. mainsequence/virtualfundbuilder/__main__.py +235 -0
  70. mainsequence/virtualfundbuilder/agent_interface.py +77 -0
  71. mainsequence/virtualfundbuilder/config_handling.py +86 -0
  72. mainsequence/virtualfundbuilder/contrib/__init__.py +0 -0
  73. mainsequence/virtualfundbuilder/contrib/apps/__init__.py +8 -0
  74. mainsequence/virtualfundbuilder/contrib/apps/etf_replicator_app.py +164 -0
  75. mainsequence/virtualfundbuilder/contrib/apps/generate_report.py +292 -0
  76. mainsequence/virtualfundbuilder/contrib/apps/load_external_portfolio.py +107 -0
  77. mainsequence/virtualfundbuilder/contrib/apps/news_app.py +437 -0
  78. mainsequence/virtualfundbuilder/contrib/apps/portfolio_report_app.py +91 -0
  79. mainsequence/virtualfundbuilder/contrib/apps/portfolio_table.py +95 -0
  80. mainsequence/virtualfundbuilder/contrib/apps/run_named_portfolio.py +45 -0
  81. mainsequence/virtualfundbuilder/contrib/apps/run_portfolio.py +40 -0
  82. mainsequence/virtualfundbuilder/contrib/apps/templates/base.html +147 -0
  83. mainsequence/virtualfundbuilder/contrib/apps/templates/report.html +77 -0
  84. mainsequence/virtualfundbuilder/contrib/data_nodes/__init__.py +5 -0
  85. mainsequence/virtualfundbuilder/contrib/data_nodes/external_weights.py +61 -0
  86. mainsequence/virtualfundbuilder/contrib/data_nodes/intraday_trend.py +149 -0
  87. mainsequence/virtualfundbuilder/contrib/data_nodes/market_cap.py +310 -0
  88. mainsequence/virtualfundbuilder/contrib/data_nodes/mock_signal.py +78 -0
  89. mainsequence/virtualfundbuilder/contrib/data_nodes/portfolio_replicator.py +269 -0
  90. mainsequence/virtualfundbuilder/contrib/prices/__init__.py +1 -0
  91. mainsequence/virtualfundbuilder/contrib/prices/data_nodes.py +810 -0
  92. mainsequence/virtualfundbuilder/contrib/prices/utils.py +11 -0
  93. mainsequence/virtualfundbuilder/contrib/rebalance_strategies/__init__.py +1 -0
  94. mainsequence/virtualfundbuilder/contrib/rebalance_strategies/rebalance_strategies.py +313 -0
  95. mainsequence/virtualfundbuilder/data_nodes.py +637 -0
  96. mainsequence/virtualfundbuilder/enums.py +23 -0
  97. mainsequence/virtualfundbuilder/models.py +282 -0
  98. mainsequence/virtualfundbuilder/notebook_handling.py +42 -0
  99. mainsequence/virtualfundbuilder/portfolio_interface.py +272 -0
  100. mainsequence/virtualfundbuilder/resource_factory/__init__.py +0 -0
  101. mainsequence/virtualfundbuilder/resource_factory/app_factory.py +170 -0
  102. mainsequence/virtualfundbuilder/resource_factory/base_factory.py +238 -0
  103. mainsequence/virtualfundbuilder/resource_factory/rebalance_factory.py +101 -0
  104. mainsequence/virtualfundbuilder/resource_factory/signal_factory.py +183 -0
  105. mainsequence/virtualfundbuilder/utils.py +381 -0
  106. mainsequence-2.0.0.dist-info/METADATA +105 -0
  107. mainsequence-2.0.0.dist-info/RECORD +110 -0
  108. mainsequence-2.0.0.dist-info/WHEEL +5 -0
  109. mainsequence-2.0.0.dist-info/licenses/LICENSE +40 -0
  110. mainsequence-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,812 @@
1
+ import pandas as pd
2
+ import datetime
3
+ from typing import Union, List, Dict, Optional, Tuple
4
+ import os
5
+ from mainsequence.logconf import logger
6
+
7
+
8
+ from mainsequence.client import (LocalTimeSerie, UniqueIdentifierRangeMap,
9
+ LocalTimeSeriesDoesNotExist,
10
+ DynamicTableDoesNotExist, DynamicTableDataSource, TDAG_CONSTANTS as CONSTANTS, DynamicTableMetaData,
11
+ UpdateStatistics, DoesNotExist)
12
+
13
+ from mainsequence.client.models_tdag import LocalTimeSerieUpdateDetails
14
+ import mainsequence.client as ms_client
15
+ import json
16
+ import threading
17
+ from concurrent.futures import Future
18
+ from .. import future_registry
19
+ from mainsequence.instrumentation import tracer, tracer_instrumentator
20
+ import inspect
21
+ import hashlib
22
+
23
+ def get_data_node_source_code(DataNodeClass: "DataNode") -> str:
24
+ """
25
+ Gets the source code of a DataNode class.
26
+
27
+ Args:
28
+ DataNodeClass: The class to get the source code for.
29
+
30
+ Returns:
31
+ The source code as a string.
32
+ """
33
+ global logger
34
+ try:
35
+ # First try the standard approach.
36
+ source = inspect.getsource(DataNodeClass)
37
+ if source.strip():
38
+ return source
39
+ except Exception:
40
+ logger.warning \
41
+ ("Your TimeSeries is not in a python module this will likely bring exceptions when running in a pipeline")
42
+ from IPython import get_ipython
43
+ # Fallback: Scan IPython's input history.
44
+ ip = get_ipython() # Get the current IPython instance.
45
+ if ip is not None:
46
+ # Retrieve the full history as a single string.
47
+ history = "\n".join(code for _, _, code in ip.history_manager.get_range())
48
+ marker = f"class {DataNodeClass.__name__}"
49
+ idx = history.find(marker)
50
+ if idx != -1:
51
+ return history[idx:]
52
+ return "Source code unavailable."
53
+
54
+ def get_data_node_source_code_git_hash(DataNodeClass: "DataNode") -> str:
55
+ """
56
+ Hashes the source code of a DataNode class using SHA-1 (Git style).
57
+
58
+ Args:
59
+ DataNodeClass: The class to hash.
60
+
61
+ Returns:
62
+ The Git-style hash of the source code.
63
+ """
64
+ data_node_class_source_code = get_data_node_source_code(DataNodeClass)
65
+ # Prepare the content for Git-style hashing
66
+ # Git hashing format: "blob <size_of_content>\0<content>"
67
+ content = f"blob {len(data_node_class_source_code)}\0{data_node_class_source_code}"
68
+ # Compute the SHA-1 hash (Git hash)
69
+ hash_object = hashlib.sha1(content.encode('utf-8'))
70
+ git_hash = hash_object.hexdigest()
71
+ return git_hash
72
+
73
+
74
+ class APIPersistManager:
75
+ """
76
+ Manages persistence for time series data accessed via an API.
77
+ It handles asynchronous fetching of metadata to avoid blocking operations.
78
+ """
79
+
80
+ def __init__(self, data_source_id: int, storage_hash: str):
81
+ """
82
+ Initializes the APIPersistManager.
83
+
84
+ Args:
85
+ data_source_id: The ID of the data source.
86
+ update_hash: The local hash identifier for the time series.
87
+ """
88
+ self.data_source_id: int = data_source_id
89
+ self.storage_hash: str = storage_hash
90
+
91
+ logger.debug(f"Initializing Time Serie {self.storage_hash} as APIDataNode")
92
+
93
+ # Create a Future to hold the local metadata when ready.
94
+ self._metadata_future = Future()
95
+ # Register the future globally.
96
+ future_registry.add_future(self._metadata_future)
97
+ # Launch the REST request in a separate, non-daemon thread.
98
+ thread = threading.Thread(target=self._init_metadata,
99
+ name=f"ApiMetaDataThread-{self.storage_hash}",
100
+ daemon=False)
101
+ thread.start()
102
+
103
+
104
+ @property
105
+ def metadata(self) -> DynamicTableMetaData:
106
+ """Lazily block and cache the result if needed."""
107
+ if not hasattr(self, '_metadata_cached'):
108
+ # This call blocks until the future is resolved.
109
+ self._metadata_cached = self._metadata_future.result()
110
+ return self._metadata_cached
111
+
112
+ def _init_metadata(self) -> None:
113
+ """
114
+ Performs the REST request to fetch local metadata asynchronously.
115
+ Sets the result or exception on the future object.
116
+ """
117
+ try:
118
+ result = DynamicTableMetaData.get_or_none(storage_hash=self.storage_hash,
119
+ data_source__id=self.data_source_id,
120
+ include_relations_detail=True
121
+ )
122
+ self._metadata_future.set_result(result)
123
+ except Exception as exc:
124
+ self._metadata_future.set_exception(exc)
125
+ finally:
126
+ # Remove the future from the global registry once done.
127
+ future_registry.remove_future(self._metadata_future)
128
+
129
+ def get_df_between_dates(self, *args, **kwargs) -> pd.DataFrame:
130
+ """
131
+ Retrieves a DataFrame from the API between specified dates.
132
+
133
+ Returns:
134
+ A pandas DataFrame with the requested data.
135
+ """
136
+ filtered_data = self.metadata.get_data_between_dates_from_api(*args, **kwargs)
137
+ if filtered_data.empty:
138
+ return filtered_data
139
+
140
+ # fix types
141
+ stc = self.metadata.sourcetableconfiguration
142
+ filtered_data[stc.time_index_name] = pd.to_datetime(filtered_data[stc.time_index_name], utc=True)
143
+ column_filter = kwargs.get("columns") or stc.column_dtypes_map.keys()
144
+ for c in column_filter:
145
+ c_type=stc.column_dtypes_map[c]
146
+ if c != stc.time_index_name:
147
+ if c_type == "object":
148
+ c_type = "str"
149
+ filtered_data[c] = filtered_data[c].astype(c_type)
150
+ filtered_data = filtered_data.set_index(stc.index_names)
151
+
152
+ return filtered_data
153
+
154
+
155
+ class PersistManager:
156
+ def __init__(self,
157
+ data_source: DynamicTableDataSource,
158
+ update_hash: str,
159
+ description: Optional[str] = None,
160
+ class_name: Optional[str] = None,
161
+ metadata: Optional[Dict] = None,
162
+ local_metadata: Optional[LocalTimeSerie] = None
163
+ ):
164
+ """
165
+ Initializes the PersistManager.
166
+
167
+ Args:
168
+ data_source: The data source for the time series.
169
+ update_hash: The local hash identifier for the time series.
170
+ description: An optional description for the time series.
171
+ class_name: The name of the DataNode class.
172
+ metadata: Optional remote metadata dictionary.
173
+ local_metadata: Optional local metadata object.
174
+ """
175
+ self.data_source: DynamicTableDataSource = data_source
176
+ self.update_hash: str = update_hash
177
+ if local_metadata is not None and metadata is None:
178
+ # query remote storage_hash
179
+ metadata = local_metadata.remote_table
180
+ self.description: Optional[str] = description
181
+ self.logger = logger
182
+
183
+ self.table_model_loaded: bool = False
184
+ self.class_name: Optional[str] = class_name
185
+
186
+ # Private members for managing lazy asynchronous retrieval.
187
+ self._local_metadata_future: Optional[Future] = None
188
+ self._local_metadata_cached: Optional[LocalTimeSerie] = None
189
+ self._local_metadata_lock = threading.Lock()
190
+ self._metadata_cached: Optional[DynamicTableMetaData] = None
191
+
192
+ if self.update_hash is not None:
193
+ self.synchronize_metadata(local_metadata=local_metadata)
194
+
195
+ def synchronize_metadata(self, local_metadata: Optional[LocalTimeSerie]) -> None:
196
+ if local_metadata is not None:
197
+ self.set_local_metadata(local_metadata)
198
+ else:
199
+ self.set_local_metadata_lazy(force_registry=True, include_relations_detail=True)
200
+
201
+ @classmethod
202
+ def get_from_data_type(cls, data_source: DynamicTableDataSource, *args, **kwargs) -> 'PersistManager':
203
+ """
204
+ Factory method to get the correct PersistManager based on data source type.
205
+
206
+ Args:
207
+ data_source: The data source object.
208
+
209
+ Returns:
210
+ An instance of a PersistManager subclass.
211
+ """
212
+ data_type = data_source.related_resource_class_type
213
+ if data_type in CONSTANTS.DATA_SOURCE_TYPE_TIMESCALEDB:
214
+ return TimeScaleLocalPersistManager(data_source=data_source, *args, **kwargs)
215
+ else:
216
+ return TimeScaleLocalPersistManager(data_source=data_source, *args, **kwargs)
217
+
218
+ def set_local_metadata(self, local_metadata: LocalTimeSerie) -> None:
219
+ """
220
+ Caches the local metadata object for lazy queries
221
+
222
+ Args:
223
+ local_metadata: The LocalTimeSerie object to cache.
224
+ """
225
+ self._local_metadata_cached = local_metadata
226
+
227
+ @property
228
+ def local_metadata(self) -> LocalTimeSerie:
229
+ """Lazily block and retrieve the local metadata, caching the result."""
230
+ with self._local_metadata_lock:
231
+ if self._local_metadata_cached is None:
232
+ if self._local_metadata_future is None:
233
+ # If no future is running, start one.
234
+ self.set_local_metadata_lazy(force_registry=True)
235
+ # Block until the future completes and cache its result.
236
+ local_metadata = self._local_metadata_future.result()
237
+ self.set_local_metadata(local_metadata)
238
+ return self._local_metadata_cached
239
+
240
+ # Define a callback that will launch set_local_metadata_lazy after the remote update is complete.
241
+ @property
242
+ def metadata(self) -> Optional[DynamicTableMetaData]:
243
+ """
244
+ Lazily retrieves and returns the remote metadata.
245
+ """
246
+ if self.local_metadata is None:
247
+ return None
248
+ if self.local_metadata.remote_table is not None:
249
+ if self.local_metadata.remote_table.sourcetableconfiguration is not None:
250
+ if self.local_metadata.remote_table.build_meta_data.get("initialize_with_default_partitions",True) == False:
251
+ if self.local_metadata.remote_table.data_source.related_resource_class_type in CONSTANTS.DATA_SOURCE_TYPE_TIMESCALEDB:
252
+ self.logger.warning("Default Partitions will not be initialized ")
253
+
254
+ return self.local_metadata.remote_table
255
+
256
+ @property
257
+ def local_build_configuration(self) -> Dict:
258
+ return self.local_metadata.build_configuration
259
+
260
+ @property
261
+ def local_build_metadata(self) -> Dict:
262
+ return self.local_metadata.build_meta_data
263
+
264
+ def set_local_metadata_lazy_callback(self, fut: Future) -> None:
265
+ """
266
+ Callback to handle the result of an asynchronous task and trigger a metadata refresh.
267
+ """
268
+ try:
269
+ # This will re-raise any exception that occurred in _update_task.
270
+ fut.result()
271
+ except Exception as exc:
272
+ # Optionally, handle or log the error if needed.
273
+ # For example: logger.error("Remote build update failed: %s", exc)
274
+ raise exc
275
+ # Launch the local metadata update regardless of the outcome.
276
+ self.set_local_metadata_lazy(force_registry=True)
277
+
278
+ def set_local_metadata_lazy(self, force_registry: bool = True, include_relations_detail: bool = True) -> None:
279
+ """
280
+ Initiates a lazy, asynchronous fetch of the local metadata.
281
+
282
+ Args:
283
+ force_registry: If True, forces a refresh even if cached data exists.
284
+ include_relations_detail: If True, includes relationship details in the fetch.
285
+ """
286
+ with self._local_metadata_lock:
287
+ if force_registry:
288
+ self._local_metadata_cached = None
289
+ # Capture the new future in a local variable.
290
+ new_future = Future()
291
+ self._local_metadata_future = new_future
292
+ # Register the new future.
293
+ future_registry.add_future(new_future)
294
+
295
+ def _get_or_none_local_metadata():
296
+ """Perform the REST request asynchronously."""
297
+ try:
298
+ result = LocalTimeSerie.get_or_none(
299
+ update_hash=self.update_hash,
300
+ remote_table__data_source__id=self.data_source.id,
301
+ include_relations_detail=include_relations_detail
302
+ )
303
+ if result is None:
304
+ self.logger.warning(f"TimeSeries {self.update_hash} with data source {self.data_source.id} not found in backend")
305
+ new_future.set_result(result)
306
+ except Exception as exc:
307
+ new_future.set_exception(exc)
308
+ finally:
309
+ # Remove the future from the global registry once done.
310
+ future_registry.remove_future(new_future)
311
+
312
+ thread = threading.Thread(target=_get_or_none_local_metadata,
313
+ name=f"LocalMetadataThreadPM-{self.update_hash}",
314
+ daemon=False)
315
+ thread.start()
316
+
317
+
318
+
319
+ def depends_on_connect(self, new_ts: "DataNode", is_api: bool) -> None:
320
+ """
321
+ Connects a time series as a relationship in the DB.
322
+
323
+ Args:
324
+ new_ts: The target DataNode to connect to.
325
+ is_api: True if the target is an APIDataNode
326
+ """
327
+ if not is_api:
328
+ self.local_metadata.depends_on_connect(target_time_serie_id=new_ts.local_time_serie.id)
329
+ else:
330
+ try:
331
+ self.local_metadata.depends_on_connect_to_api_table(target_table_id=new_ts.local_persist_manager.metadata.id)
332
+ except Exception as exc:
333
+ raise exc
334
+
335
+ def display_mermaid_dependency_diagram(self) -> str:
336
+ """
337
+ Generates and returns an HTML string for a Mermaid dependency diagram.
338
+
339
+ Returns:
340
+ An HTML string containing the Mermaid diagram and supporting Javascript.
341
+ """
342
+ from IPython.core.display import display, HTML, Javascript
343
+
344
+ response = ms_client.TimeSerieLocalUpdate.get_mermaid_dependency_diagram(update_hash=self.update_hash,
345
+ data_source_id=self.data_source.id
346
+ )
347
+ from IPython.core.display import display, HTML, Javascript
348
+ mermaid_chart = response.get("mermaid_chart")
349
+ metadata = response.get("metadata")
350
+ # Render Mermaid.js diagram with metadata display
351
+ html_template = f"""
352
+ <div class="mermaid">
353
+ {mermaid_chart}
354
+ </div>
355
+ <div id="metadata-display" style="margin-top: 20px; font-size: 16px; color: #333;"></div>
356
+ <script>
357
+ // Initialize Mermaid.js
358
+ if (typeof mermaid !== 'undefined') {{
359
+ mermaid.initialize({{ startOnLoad: true }});
360
+ }}
361
+
362
+ // Metadata dictionary
363
+ const metadata = {metadata};
364
+
365
+ // Attach click listeners to nodes
366
+ document.addEventListener('click', function(event) {{
367
+ const target = event.target.closest('div[data-graph-id]');
368
+ if (target) {{
369
+ const nodeId = target.dataset.graphId;
370
+ const metadataDisplay = document.getElementById('metadata-display');
371
+ if (metadata[nodeId]) {{
372
+ metadataDisplay.innerHTML = "<strong>Node Metadata:</strong> " + metadata[nodeId];
373
+ }} else {{
374
+ metadataDisplay.innerHTML = "<strong>No metadata available for this node.</strong>";
375
+ }}
376
+ }}
377
+ }});
378
+ </script>
379
+ """
380
+
381
+ return mermaid_chart
382
+
383
+ def get_mermaid_dependency_diagram(self) -> str:
384
+ """
385
+ Displays a Mermaid.js dependency diagram in a Jupyter environment.
386
+
387
+ Returns:
388
+ The Mermaid diagram string.
389
+ """
390
+ from IPython.display import display, HTML
391
+
392
+ mermaid_diagram = self.display_mermaid_dependency_diagram()
393
+
394
+ # Mermaid.js initialization script (only run once)
395
+ if not hasattr(display, "_mermaid_initialized"):
396
+ mermaid_initialize = """
397
+ <script src="https://cdn.jsdelivr.net/npm/mermaid/dist/mermaid.min.js"></script>
398
+ <script>
399
+ function initializeMermaid() {
400
+ if (typeof mermaid !== 'undefined') {
401
+ console.log('Initializing Mermaid.js...');
402
+ const mermaidDivs = document.querySelectorAll('.mermaid');
403
+ mermaidDivs.forEach(mermaidDiv => {
404
+ mermaid.init(undefined, mermaidDiv);
405
+ });
406
+ } else {
407
+ console.error('Mermaid.js is not loaded.');
408
+ }
409
+ }
410
+ </script>
411
+ """
412
+ display(HTML(mermaid_initialize))
413
+ display._mermaid_initialized = True
414
+
415
+ # HTML template for rendering the Mermaid diagram
416
+ html_template = f"""
417
+ <div class="mermaid">
418
+ {mermaid_diagram}
419
+ </div>
420
+ <script>
421
+ initializeMermaid();
422
+ </script>
423
+ """
424
+
425
+ # Display the Mermaid diagram in the notebook
426
+ display(HTML(html_template))
427
+
428
+ # Optionally return the raw diagram code for further use
429
+ return mermaid_diagram
430
+
431
+ def get_all_dependencies_update_priority(self) -> pd.DataFrame:
432
+ """
433
+ Retrieves a DataFrame of all dependencies with their update priority.
434
+
435
+ Returns:
436
+ A pandas DataFrame with dependency and priority information.
437
+ """
438
+ depth_df = self.local_metadata.get_all_dependencies_update_priority()
439
+ return depth_df
440
+
441
+ def set_ogm_dependencies_linked(self) -> None:
442
+ self.local_metadata.patch(ogm_dependencies_linked=True)
443
+
444
+ @property
445
+ def update_details(self) -> Optional[LocalTimeSerieUpdateDetails]:
446
+ """Returns the update details associated with the local time series."""
447
+ return self.local_metadata.localtimeserieupdatedetails
448
+
449
+ @property
450
+ def run_configuration(self) -> Optional[Dict]:
451
+ """Returns the run configuration from the local metadata."""
452
+ return self.local_metadata.run_configuration
453
+
454
+ @property
455
+ def source_table_configuration(self) -> Optional[Dict]:
456
+ """Returns the source table configuration from the remote metadata."""
457
+ if "sourcetableconfiguration" in self.metadata.keys():
458
+ return self.metadata['sourcetableconfiguration']
459
+ return None
460
+
461
+ def update_source_informmation(self, git_hash_id: str, source_code: str) -> None:
462
+ """
463
+ Updates the source code and git hash for the remote table.
464
+ """
465
+ self.local_metadata.remote_table = self.metadata.patch(
466
+ time_serie_source_code_git_hash=git_hash_id,
467
+ time_serie_source_code=source_code,
468
+ )
469
+
470
+
471
+
472
+ def add_tags(self, tags: List[str]) -> None:
473
+ """Adds tags to the local time series metadata if they don't already exist."""
474
+ if any([t not in self.local_metadata.tags for t in tags]) == True:
475
+ self.local_metadata.add_tags(tags=tags)
476
+
477
+ @property
478
+ def persist_size(self) -> int:
479
+ """Returns the size of the persisted table, or 0 if not available."""
480
+ try:
481
+ return self.metadata['table_size']
482
+ except KeyError:
483
+ return 0
484
+
485
+ def time_serie_exist(self) -> bool:
486
+ """Checks if the remote metadata for the time series exists."""
487
+ if hasattr(self, "metadata"):
488
+ return True
489
+ return False
490
+
491
+ def patch_build_configuration(self, local_configuration: dict, remote_configuration: dict,
492
+ remote_build_metadata: dict) -> None:
493
+ """
494
+ Asynchronously patches the build configuration for the remote and local tables.
495
+
496
+ Args:
497
+ local_configuration: The build configuration for the local time series.
498
+ remote_configuration: The build configuration for the remote table.
499
+ remote_build_metadata: The build metadata for the remote table.
500
+ """
501
+ # This ensures that later accesses to local_metadata will block for the new value.
502
+ with self._local_metadata_lock:
503
+ self._local_metadata_future = Future()
504
+ future_registry.add_future(self._local_metadata_future)
505
+
506
+ kwargs = dict(
507
+ build_configuration=remote_configuration, )
508
+
509
+
510
+ local_metadata_kwargs = dict(update_hash=self.update_hash,
511
+ build_configuration=local_configuration,
512
+ )
513
+
514
+ patch_future = Future()
515
+ future_registry.add_future(patch_future)
516
+
517
+ # Define the inner helper function.
518
+ def _patch_build_configuration():
519
+ """Helper function for patching build configuration asynchronously."""
520
+ try:
521
+ # Execute the patch operation; this method is expected to return a LocalTimeSerie-like instance.
522
+ result = DynamicTableMetaData.patch_build_configuration(
523
+ remote_table_patch=kwargs,
524
+ data_source_id=self.data_source.id,
525
+ build_meta_data=remote_build_metadata,
526
+ local_table_patch=local_metadata_kwargs,
527
+ )
528
+ patch_future.set_result(True) #success
529
+ except Exception as exc:
530
+ patch_future.set_exception(exc)
531
+ finally:
532
+ # Once the operation is complete (or errors out), remove the future from the global registry.
533
+ future_registry.remove_future(result)
534
+
535
+ thread = threading.Thread(
536
+ target=_patch_build_configuration,
537
+ name=f"PatchBuildConfigThread-{self.update_hash}",
538
+ daemon=False
539
+ )
540
+ thread.start()
541
+
542
+ patch_future.add_done_callback(self.set_local_metadata_lazy_callback)
543
+
544
+
545
+ def local_persist_exist_set_config(
546
+ self,
547
+ storage_hash: str,
548
+ local_configuration: dict,
549
+ remote_configuration: dict,
550
+ data_source: DynamicTableDataSource,
551
+ time_serie_source_code_git_hash: str,
552
+ time_serie_source_code: str,
553
+ build_configuration_json_schema: dict,
554
+ ) -> None:
555
+ """
556
+ Ensures local and remote persistence objects exist and sets their configurations.
557
+ This runs on DataNode initialization.
558
+ """
559
+ remote_build_configuration = None
560
+ if hasattr(self, "remote_build_configuration"):
561
+ remote_build_configuration = self.remote_build_configuration
562
+
563
+ if remote_build_configuration is None:
564
+ logger.debug(f"remote table {storage_hash} does not exist creating")
565
+ #create remote table
566
+
567
+ try:
568
+
569
+ # table may not exist but
570
+ remote_build_metadata = remote_configuration["build_meta_data"] if "build_meta_data" in remote_configuration.keys() else {}
571
+ remote_configuration.pop("build_meta_data", None)
572
+ kwargs = dict(storage_hash=storage_hash,
573
+ time_serie_source_code_git_hash=time_serie_source_code_git_hash,
574
+ time_serie_source_code=time_serie_source_code,
575
+ build_configuration=remote_configuration,
576
+ data_source=data_source.model_dump(),
577
+ build_meta_data=remote_build_metadata,
578
+ build_configuration_json_schema=build_configuration_json_schema
579
+ )
580
+
581
+
582
+ dtd_metadata = DynamicTableMetaData.get_or_create(**kwargs)
583
+ storage_hash = dtd_metadata.storage_hash
584
+ except Exception as e:
585
+ self.logger.exception(f"{storage_hash} Could not set meta data in DB for P")
586
+ raise e
587
+ else:
588
+ self.set_local_metadata_lazy(force_registry=True, include_relations_detail=True)
589
+ storage_hash = self.metadata.storage_hash
590
+
591
+ local_table_exist = self._verify_local_ts_exists(storage_hash=storage_hash, local_configuration=local_configuration)
592
+
593
+
594
+ def _verify_local_ts_exists(self, storage_hash: str,
595
+ local_configuration: Optional[Dict] = None) -> None:
596
+ """
597
+ Verifies that the local time series exists in the ORM, creating it if necessary.
598
+ """
599
+ local_build_configuration = None
600
+ if self.local_metadata is not None:
601
+ local_build_configuration, local_build_metadata = self.local_build_configuration, self.local_build_metadata
602
+ if local_build_configuration is None:
603
+
604
+ logger.debug(f"local_metadata {self.update_hash} does not exist creating")
605
+ local_update = LocalTimeSerie.get_or_none(update_hash=self.update_hash,
606
+ remote_table__data_source__id=self.data_source.id)
607
+ if local_update is None:
608
+ local_build_metadata = local_configuration[
609
+ "build_meta_data"] if "build_meta_data" in local_configuration.keys() else {}
610
+ local_configuration.pop("build_meta_data", None)
611
+ metadata_kwargs = dict(
612
+ update_hash=self.update_hash,
613
+ build_configuration=local_configuration,
614
+ remote_table__hash_id=storage_hash,
615
+ data_source_id=self.data_source.id
616
+ )
617
+
618
+ local_metadata = LocalTimeSerie.get_or_create(**metadata_kwargs,)
619
+ else:
620
+ local_metadata = local_update
621
+
622
+ self.set_local_metadata(local_metadata=local_metadata)
623
+
624
+
625
+ def _verify_insertion_format(self, temp_df: pd.DataFrame) -> None:
626
+ """
627
+ Verifies that a DataFrame is properly configured for insertion.
628
+ """
629
+ if isinstance(temp_df.index,pd.MultiIndex)==True:
630
+ assert temp_df.index.names==["time_index", "asset_symbol"] or temp_df.index.names==["time_index", "asset_symbol", "execution_venue_symbol"]
631
+
632
+ def build_update_details(self, source_class_name: str) -> None:
633
+ """
634
+ Asynchronously builds or updates the update details for the time series.
635
+ """
636
+ update_kwargs=dict(source_class_name=source_class_name,
637
+ local_metadata=json.loads(self.local_metadata.model_dump_json())
638
+ )
639
+ # This ensures that later accesses to local_metadata will block for the new value.
640
+ with self._local_metadata_lock:
641
+ self._local_metadata_future = Future()
642
+ future_registry.add_future(self._local_metadata_future)
643
+
644
+ # Create a future for the remote update task and register it.
645
+ future = Future()
646
+ future_registry.add_future(future)
647
+
648
+ def _update_task():
649
+ try:
650
+ # Run the remote build/update details task.
651
+ self.local_metadata.remote_table.build_or_update_update_details(**update_kwargs)
652
+ future.set_result(True) # Signal success
653
+ except Exception as exc:
654
+ future.set_exception(exc)
655
+ finally:
656
+ # Unregister the future once the task completes.
657
+ future_registry.remove_future(future)
658
+
659
+ thread = threading.Thread(
660
+ target=_update_task,
661
+ name=f"BuildUpdateDetailsThread-{self.update_hash}",
662
+ daemon=False
663
+ )
664
+ thread.start()
665
+
666
+ # Attach the callback to the future.
667
+ future.add_done_callback(self.set_local_metadata_lazy_callback)
668
+
669
+ def patch_table(self, **kwargs) -> None:
670
+ """Patches the remote metadata table with the given keyword arguments."""
671
+ self.metadata.patch( **kwargs)
672
+
673
+ def protect_from_deletion(self, protect_from_deletion: bool = True) -> None:
674
+ """Sets the 'protect_from_deletion' flag on the remote metadata."""
675
+ self.metadata.patch( protect_from_deletion=protect_from_deletion)
676
+
677
+ def open_for_everyone(self, open_for_everyone: bool = True) -> None:
678
+ """Sets the 'open_for_everyone' flag on local, remote, and source table configurations."""
679
+ if not self.local_metadata.open_for_everyone:
680
+ self.local_metadata.patch(open_for_everyone=open_for_everyone)
681
+
682
+ if not self.metadata.open_for_everyone:
683
+ self.metadata.patch(open_for_everyone=open_for_everyone)
684
+
685
+ if not self.metadata.sourcetableconfiguration.open_for_everyone:
686
+ self.metadata.sourcetableconfiguration.patch(open_for_everyone=open_for_everyone)
687
+
688
+
689
+
690
+
691
+
692
+ def get_df_between_dates(self, *args, **kwargs) -> pd.DataFrame:
693
+ """
694
+ Retrieves a DataFrame from the data source between specified dates.
695
+ """
696
+ filtered_data = self.data_source.get_data_by_time_index(
697
+ local_metadata=self.local_metadata,
698
+ *args, **kwargs
699
+ )
700
+ return filtered_data
701
+
702
+ def set_column_metadata(self,
703
+ columns_metadata: Optional[List[ms_client.ColumnMetaData]]
704
+ ) -> None:
705
+ if self.metadata:
706
+ if self.metadata.sourcetableconfiguration != None:
707
+ if self.metadata.sourcetableconfiguration.columns_metadata is not None:
708
+ if columns_metadata is None:
709
+ self.logger.info(f"get_column_metadata method not implemented")
710
+ return
711
+
712
+ self.metadata.sourcetableconfiguration.set_or_update_columns_metadata(
713
+ columns_metadata=columns_metadata)
714
+
715
+ def set_table_metadata(self,
716
+ table_metadata: ms_client.TableMetaData,
717
+ ):
718
+ """
719
+ Creates or updates the MarketsTimeSeriesDetails metadata in the backend.
720
+
721
+ This method orchestrates the synchronization of the time series metadata,
722
+ including its description, frequency, and associated assets, based on the
723
+ configuration returned by `_get_time_series_meta_details`.
724
+ """
725
+ if not (self.metadata):
726
+ self.logger.warning("metadata not set")
727
+ return
728
+
729
+ # 1. Get the user-defined metadata configuration for the time series.
730
+ if table_metadata is None:
731
+ return
732
+
733
+ # 2. Get or create the MarketsTimeSeriesDetails object in the backend.
734
+ source_table_id = self.metadata.patch(**table_metadata.model_dump())
735
+
736
+ def delete_table(self) -> None:
737
+ if self.data_source.related_resource.class_type == "duck_db":
738
+ from mainsequence.client.data_sources_interfaces.duckdb import DuckDBInterface
739
+ db_interface = DuckDBInterface()
740
+ db_interface.drop_table(self.metadata.storage_hash)
741
+
742
+ self.metadata.delete()
743
+
744
+ @tracer.start_as_current_span("TS: Persist Data")
745
+ def persist_updated_data(self,
746
+ temp_df: pd.DataFrame, overwrite: bool = False) -> bool:
747
+ """
748
+ Persists the updated data to the database.
749
+
750
+ Args:
751
+ temp_df: The DataFrame with updated data.
752
+ update_tracker: The update tracker object.
753
+ overwrite: If True, overwrites existing data.
754
+
755
+ Returns:
756
+ True if data was persisted, False otherwise.
757
+ """
758
+ persisted = False
759
+ if not temp_df.empty:
760
+ if overwrite == True:
761
+ self.logger.warning(f"Values will be overwritten")
762
+
763
+ self._local_metadata_cached = self.local_metadata.upsert_data_into_table(
764
+ data=temp_df,
765
+ data_source=self.data_source,
766
+
767
+ )
768
+
769
+
770
+ persisted = True
771
+ return persisted
772
+
773
+ def get_update_statistics_for_table(self) -> ms_client.UpdateStatistics:
774
+ """
775
+ Gets the latest update statistics from the database.
776
+
777
+ Args:
778
+ unique_identifier_list: An optional list of unique identifiers to filter by.
779
+
780
+ Returns:
781
+ A UpdateStatistics object with the latest statistics.
782
+ """
783
+ if isinstance(self.metadata, int):
784
+ self.set_local_metadata_lazy(force_registry=True, include_relations_detail=True)
785
+
786
+ if self.metadata.sourcetableconfiguration is None:
787
+ return ms_client.UpdateStatistics()
788
+
789
+ update_stats = self.metadata.sourcetableconfiguration.get_data_updates()
790
+ return update_stats
791
+
792
+ def is_local_relation_tree_set(self) -> bool:
793
+ return self.local_metadata.ogm_dependencies_linked
794
+
795
+
796
+
797
+ def update_git_and_code_in_backend(self,time_serie_class) -> None:
798
+ """Updates the source code and git hash information in the backend."""
799
+ self.update_source_informmation(
800
+ git_hash_id=get_data_node_source_code_git_hash(time_serie_class),
801
+ source_code=get_data_node_source_code(time_serie_class),
802
+ )
803
+
804
+ class TimeScaleLocalPersistManager(PersistManager):
805
+ """
806
+ Main Controler to interacti with backend
807
+ """
808
+ def get_table_schema(self,table_name):
809
+ return self.metadata["sourcetableconfiguration"]["column_dtypes_map"]
810
+
811
+
812
+