cognite-neat 0.104.0__py3-none-any.whl → 0.105.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 cognite-neat might be problematic. Click here for more details.

Files changed (141) hide show
  1. cognite/neat/_client/_api/data_modeling_loaders.py +83 -23
  2. cognite/neat/_client/_api/schema.py +2 -1
  3. cognite/neat/_client/data_classes/neat_sequence.py +261 -0
  4. cognite/neat/_client/data_classes/schema.py +5 -1
  5. cognite/neat/_client/testing.py +33 -0
  6. cognite/neat/_constants.py +56 -0
  7. cognite/neat/_graph/extractors/_classic_cdf/_base.py +6 -5
  8. cognite/neat/_graph/extractors/_classic_cdf/_sequences.py +225 -11
  9. cognite/neat/_graph/extractors/_mock_graph_generator.py +1 -1
  10. cognite/neat/_graph/loaders/_rdf2dms.py +13 -2
  11. cognite/neat/_graph/transformers/__init__.py +3 -1
  12. cognite/neat/_graph/transformers/_classic_cdf.py +2 -1
  13. cognite/neat/_graph/transformers/_value_type.py +72 -0
  14. cognite/neat/_issues/__init__.py +0 -2
  15. cognite/neat/_issues/_base.py +19 -35
  16. cognite/neat/_issues/warnings/__init__.py +4 -1
  17. cognite/neat/_issues/warnings/_general.py +7 -0
  18. cognite/neat/_issues/warnings/_resources.py +11 -0
  19. cognite/neat/_rules/exporters/_rules2dms.py +35 -1
  20. cognite/neat/_rules/exporters/_rules2excel.py +2 -2
  21. cognite/neat/_rules/importers/_dms2rules.py +66 -55
  22. cognite/neat/_rules/models/_base_rules.py +4 -1
  23. cognite/neat/_rules/models/entities/_wrapped.py +10 -5
  24. cognite/neat/_rules/models/mapping/_classic2core.yaml +239 -38
  25. cognite/neat/_rules/transformers/__init__.py +8 -2
  26. cognite/neat/_rules/transformers/_converters.py +271 -188
  27. cognite/neat/_rules/transformers/_mapping.py +75 -59
  28. cognite/neat/_rules/transformers/_verification.py +2 -3
  29. cognite/neat/_session/_inspect.py +3 -1
  30. cognite/neat/_session/_prepare.py +112 -24
  31. cognite/neat/_session/_read.py +33 -70
  32. cognite/neat/_session/_state.py +2 -2
  33. cognite/neat/_session/_to.py +2 -2
  34. cognite/neat/_store/_rules_store.py +4 -8
  35. cognite/neat/_utils/reader/_base.py +27 -0
  36. cognite/neat/_version.py +1 -1
  37. {cognite_neat-0.104.0.dist-info → cognite_neat-0.105.0.dist-info}/METADATA +3 -2
  38. cognite_neat-0.105.0.dist-info/RECORD +179 -0
  39. {cognite_neat-0.104.0.dist-info → cognite_neat-0.105.0.dist-info}/WHEEL +1 -1
  40. cognite/neat/_app/api/__init__.py +0 -0
  41. cognite/neat/_app/api/asgi/metrics.py +0 -4
  42. cognite/neat/_app/api/configuration.py +0 -98
  43. cognite/neat/_app/api/context_manager/__init__.py +0 -3
  44. cognite/neat/_app/api/context_manager/manager.py +0 -16
  45. cognite/neat/_app/api/data_classes/__init__.py +0 -0
  46. cognite/neat/_app/api/data_classes/rest.py +0 -59
  47. cognite/neat/_app/api/explorer.py +0 -66
  48. cognite/neat/_app/api/routers/configuration.py +0 -25
  49. cognite/neat/_app/api/routers/crud.py +0 -102
  50. cognite/neat/_app/api/routers/metrics.py +0 -10
  51. cognite/neat/_app/api/routers/workflows.py +0 -224
  52. cognite/neat/_app/api/utils/__init__.py +0 -0
  53. cognite/neat/_app/api/utils/data_mapping.py +0 -17
  54. cognite/neat/_app/api/utils/logging.py +0 -26
  55. cognite/neat/_app/api/utils/query_templates.py +0 -92
  56. cognite/neat/_app/main.py +0 -17
  57. cognite/neat/_app/monitoring/__init__.py +0 -0
  58. cognite/neat/_app/monitoring/metrics.py +0 -69
  59. cognite/neat/_app/ui/index.html +0 -1
  60. cognite/neat/_app/ui/neat-app/.gitignore +0 -23
  61. cognite/neat/_app/ui/neat-app/README.md +0 -70
  62. cognite/neat/_app/ui/neat-app/build/asset-manifest.json +0 -14
  63. cognite/neat/_app/ui/neat-app/build/favicon.ico +0 -0
  64. cognite/neat/_app/ui/neat-app/build/img/architect-icon.svg +0 -116
  65. cognite/neat/_app/ui/neat-app/build/img/developer-icon.svg +0 -112
  66. cognite/neat/_app/ui/neat-app/build/img/sme-icon.svg +0 -34
  67. cognite/neat/_app/ui/neat-app/build/index.html +0 -1
  68. cognite/neat/_app/ui/neat-app/build/logo192.png +0 -0
  69. cognite/neat/_app/ui/neat-app/build/manifest.json +0 -25
  70. cognite/neat/_app/ui/neat-app/build/robots.txt +0 -3
  71. cognite/neat/_app/ui/neat-app/build/static/css/main.72e3d92e.css +0 -2
  72. cognite/neat/_app/ui/neat-app/build/static/css/main.72e3d92e.css.map +0 -1
  73. cognite/neat/_app/ui/neat-app/build/static/js/main.5a52cf09.js +0 -3
  74. cognite/neat/_app/ui/neat-app/build/static/js/main.5a52cf09.js.LICENSE.txt +0 -88
  75. cognite/neat/_app/ui/neat-app/build/static/js/main.5a52cf09.js.map +0 -1
  76. cognite/neat/_app/ui/neat-app/build/static/media/logo.8093b84df9ed36a174c629d6fe0b730d.svg +0 -1
  77. cognite/neat/_app/ui/neat-app/package-lock.json +0 -18306
  78. cognite/neat/_app/ui/neat-app/package.json +0 -62
  79. cognite/neat/_app/ui/neat-app/public/favicon.ico +0 -0
  80. cognite/neat/_app/ui/neat-app/public/img/architect-icon.svg +0 -116
  81. cognite/neat/_app/ui/neat-app/public/img/developer-icon.svg +0 -112
  82. cognite/neat/_app/ui/neat-app/public/img/sme-icon.svg +0 -34
  83. cognite/neat/_app/ui/neat-app/public/index.html +0 -43
  84. cognite/neat/_app/ui/neat-app/public/logo192.png +0 -0
  85. cognite/neat/_app/ui/neat-app/public/manifest.json +0 -25
  86. cognite/neat/_app/ui/neat-app/public/robots.txt +0 -3
  87. cognite/neat/_app/ui/neat-app/src/App.css +0 -38
  88. cognite/neat/_app/ui/neat-app/src/App.js +0 -17
  89. cognite/neat/_app/ui/neat-app/src/App.test.js +0 -8
  90. cognite/neat/_app/ui/neat-app/src/MainContainer.tsx +0 -70
  91. cognite/neat/_app/ui/neat-app/src/components/JsonViewer.tsx +0 -43
  92. cognite/neat/_app/ui/neat-app/src/components/LocalUploader.tsx +0 -124
  93. cognite/neat/_app/ui/neat-app/src/components/OverviewComponentEditorDialog.tsx +0 -63
  94. cognite/neat/_app/ui/neat-app/src/components/StepEditorDialog.tsx +0 -511
  95. cognite/neat/_app/ui/neat-app/src/components/TabPanel.tsx +0 -36
  96. cognite/neat/_app/ui/neat-app/src/components/Utils.tsx +0 -56
  97. cognite/neat/_app/ui/neat-app/src/components/WorkflowDeleteDialog.tsx +0 -60
  98. cognite/neat/_app/ui/neat-app/src/components/WorkflowExecutionReport.tsx +0 -112
  99. cognite/neat/_app/ui/neat-app/src/components/WorkflowImportExportDialog.tsx +0 -67
  100. cognite/neat/_app/ui/neat-app/src/components/WorkflowMetadataDialog.tsx +0 -79
  101. cognite/neat/_app/ui/neat-app/src/index.css +0 -13
  102. cognite/neat/_app/ui/neat-app/src/index.js +0 -13
  103. cognite/neat/_app/ui/neat-app/src/logo.svg +0 -1
  104. cognite/neat/_app/ui/neat-app/src/reportWebVitals.js +0 -13
  105. cognite/neat/_app/ui/neat-app/src/setupTests.js +0 -5
  106. cognite/neat/_app/ui/neat-app/src/types/WorkflowTypes.ts +0 -388
  107. cognite/neat/_app/ui/neat-app/src/views/AboutView.tsx +0 -61
  108. cognite/neat/_app/ui/neat-app/src/views/ConfigView.tsx +0 -184
  109. cognite/neat/_app/ui/neat-app/src/views/GlobalConfigView.tsx +0 -180
  110. cognite/neat/_app/ui/neat-app/src/views/WorkflowView.tsx +0 -570
  111. cognite/neat/_app/ui/neat-app/tsconfig.json +0 -27
  112. cognite/neat/_workflows/__init__.py +0 -17
  113. cognite/neat/_workflows/base.py +0 -590
  114. cognite/neat/_workflows/cdf_store.py +0 -393
  115. cognite/neat/_workflows/examples/Export_DMS/workflow.yaml +0 -89
  116. cognite/neat/_workflows/examples/Export_Semantic_Data_Model/workflow.yaml +0 -66
  117. cognite/neat/_workflows/examples/Import_DMS/workflow.yaml +0 -65
  118. cognite/neat/_workflows/examples/Validate_Rules/workflow.yaml +0 -67
  119. cognite/neat/_workflows/examples/Validate_Solution_Model/workflow.yaml +0 -64
  120. cognite/neat/_workflows/manager.py +0 -292
  121. cognite/neat/_workflows/model.py +0 -203
  122. cognite/neat/_workflows/steps/__init__.py +0 -0
  123. cognite/neat/_workflows/steps/data_contracts.py +0 -109
  124. cognite/neat/_workflows/steps/lib/__init__.py +0 -0
  125. cognite/neat/_workflows/steps/lib/current/__init__.py +0 -6
  126. cognite/neat/_workflows/steps/lib/current/graph_extractor.py +0 -100
  127. cognite/neat/_workflows/steps/lib/current/graph_loader.py +0 -51
  128. cognite/neat/_workflows/steps/lib/current/graph_store.py +0 -48
  129. cognite/neat/_workflows/steps/lib/current/rules_exporter.py +0 -537
  130. cognite/neat/_workflows/steps/lib/current/rules_importer.py +0 -323
  131. cognite/neat/_workflows/steps/lib/current/rules_validator.py +0 -106
  132. cognite/neat/_workflows/steps/lib/io/__init__.py +0 -1
  133. cognite/neat/_workflows/steps/lib/io/io_steps.py +0 -393
  134. cognite/neat/_workflows/steps/step_model.py +0 -79
  135. cognite/neat/_workflows/steps_registry.py +0 -218
  136. cognite/neat/_workflows/tasks.py +0 -18
  137. cognite/neat/_workflows/triggers.py +0 -169
  138. cognite/neat/_workflows/utils.py +0 -19
  139. cognite_neat-0.104.0.dist-info/RECORD +0 -276
  140. {cognite_neat-0.104.0.dist-info → cognite_neat-0.105.0.dist-info}/LICENSE +0 -0
  141. {cognite_neat-0.104.0.dist-info → cognite_neat-0.105.0.dist-info}/entry_points.txt +0 -0
@@ -1,590 +0,0 @@
1
- import json
2
- import logging
3
- import os
4
- import threading
5
- import time
6
- import traceback
7
- from pathlib import Path
8
- from threading import Event
9
-
10
- import yaml
11
- from cognite.client import ClientConfig, CogniteClient
12
- from prometheus_client import Gauge
13
-
14
- from cognite.neat._app.monitoring.metrics import NeatMetricsCollector
15
- from cognite.neat._config import Config
16
- from cognite.neat._issues.errors import WorkflowConfigurationNotSetError, WorkflowStepOutputError
17
- from cognite.neat._utils.auxiliary import retry_decorator
18
- from cognite.neat._workflows import cdf_store, utils
19
- from cognite.neat._workflows.cdf_store import CdfStore
20
- from cognite.neat._workflows.model import (
21
- FlowMessage,
22
- StepExecutionStatus,
23
- StepType,
24
- WorkflowConfigItem,
25
- WorkflowConfigs,
26
- WorkflowDefinition,
27
- WorkflowFullStateReport,
28
- WorkflowStartException,
29
- WorkflowState,
30
- WorkflowStepDefinition,
31
- WorkflowStepEvent,
32
- WorkflowSystemComponent,
33
- )
34
- from cognite.neat._workflows.steps.step_model import DataContract
35
- from cognite.neat._workflows.steps_registry import StepsRegistry
36
- from cognite.neat._workflows.tasks import WorkflowTaskBuilder
37
-
38
- summary_metrics = Gauge("neat_workflow_summary_metrics", "Workflow execution summary metrics", ["wf_name", "name"])
39
- steps_metrics = Gauge("neat_workflow_steps_metrics", "Workflow step level metrics", ["wf_name", "step_name", "name"])
40
- timing_metrics = Gauge("neat_workflow_timing_metrics", "Workflow timing metrics", ["wf_name", "component", "name"])
41
-
42
-
43
- class BaseWorkflow:
44
- def __init__(
45
- self,
46
- name: str,
47
- client: CogniteClient,
48
- steps_registry: StepsRegistry,
49
- workflow_steps: list[WorkflowStepDefinition] | None = None,
50
- default_dataset_id: int | None = None,
51
- ):
52
- self.name = name
53
- self.module_name = self.__class__.__module__
54
- self.cdf_client = client
55
- self.cdf_client_config: ClientConfig = client.config
56
- self.default_dataset_id = default_dataset_id
57
- self.state = WorkflowState.CREATED
58
- self.instance_id = utils.generate_run_id()
59
- self.run_id = ""
60
- self.last_error = ""
61
- self.elapsed_time: float = 0.0
62
- self.start_time: float | None = None
63
- self.end_time: float | None = None
64
- self.execution_log: list[WorkflowStepEvent] = []
65
- self.workflow_steps: list[WorkflowStepDefinition] = workflow_steps or []
66
- self.workflow_system_components: list[WorkflowSystemComponent] = []
67
- self.configs: list[WorkflowConfigItem] = []
68
- self.flow_message: FlowMessage | None = None
69
- self.task_builder: WorkflowTaskBuilder | None = None
70
- self.rules_storage_path: Path | None = None
71
- self.data_store_path: Path | None = None
72
- self.user_steps_path: Path | None = None # path to user defined steps
73
- self.cdf_store = (
74
- cdf_store.CdfStore(self.cdf_client, data_set_id=self.default_dataset_id)
75
- if self.default_dataset_id
76
- else None
77
- )
78
- self.metrics = NeatMetricsCollector(self.name, self.cdf_client)
79
- self.resume_event = Event()
80
- self.is_ephemeral = False # if True, workflow will be deleted after completion
81
- self.auto_workflow_cleanup = False
82
- self.step_classes = None
83
- self.data: dict[str, DataContract | FlowMessage | CdfStore | CogniteClient | None] = {}
84
- self.steps_registry: StepsRegistry = steps_registry
85
-
86
- def start(self, sync=False, is_ephemeral=False, **kwargs) -> FlowMessage | None:
87
- """Starts workflow execution.sync=True will block until workflow is completed and
88
- return last workflow flow message, sync=False will start workflow in a separate thread and return None"""
89
- if self.state not in [WorkflowState.CREATED, WorkflowState.COMPLETED, WorkflowState.FAILED]:
90
- logging.error(f"Workflow {self.name} is already running")
91
- return None
92
- self.data["StartFlowMessage"] = kwargs.get("flow_message", None)
93
- self.data["CdfStore"] = self.cdf_store
94
- self.data["CogniteClient"] = self.cdf_client
95
- self.state = WorkflowState.RUNNING
96
- self.start_time = time.time()
97
- self.end_time = None
98
- self.run_id = utils.generate_run_id()
99
- self.is_ephemeral = is_ephemeral
100
- self.execution_log = []
101
-
102
- if sync:
103
- return self._run_workflow(**kwargs)
104
-
105
- self.thread = threading.Thread(target=self._run_workflow, kwargs=kwargs)
106
- self.thread.start()
107
- return None
108
-
109
- def _run_workflow(self, **kwargs) -> FlowMessage | None:
110
- """Run workflow and return last workflow flow message"""
111
- summary_metrics.labels(wf_name=self.name, name="steps_count").set(len(self.workflow_steps))
112
- logging.info(f"Starting workflow {self.name}")
113
- if flow_message := kwargs.get("flow_message"):
114
- self.flow_message = flow_message
115
- self.data["FlowMessage"] = flow_message
116
- start_time = time.perf_counter()
117
- self.report_workflow_execution()
118
- try:
119
- start_step_id = kwargs.get("start_step_id")
120
- logging.info(f" starting workflow from step {start_step_id}")
121
-
122
- self.run_workflow_steps(start_step_id=start_step_id)
123
- if self.state == WorkflowState.RUNNING:
124
- self.state = WorkflowState.COMPLETED
125
- summary_metrics.labels(wf_name=self.name, name="wf_completed_counter").inc()
126
- except Exception:
127
- trace = traceback.format_exc()
128
- self.last_error = str(trace)
129
- self.state = WorkflowState.FAILED
130
- logging.error(f"Workflow failed with error {trace}")
131
- summary_metrics.labels(wf_name=self.name, name="wf_failed_counter").inc()
132
-
133
- self.elapsed_time = time.perf_counter() - start_time
134
- self.end_time = time.time()
135
- timing_metrics.labels(wf_name=self.name, component="workflow", name="workflow").set(self.elapsed_time)
136
- logging.info(f"Workflow completed in {self.elapsed_time} seconds")
137
- self.report_workflow_execution()
138
- if self.auto_workflow_cleanup:
139
- self.cleanup_workflow_context()
140
- return self.flow_message
141
-
142
- def cleanup_workflow_context(self):
143
- """
144
- Cleans up the workflow context by closing and deleting graph stores and other structure.
145
- It's a for of garbage collection to avoid memory leaks or other issues related to open handlers
146
- or allocated resources
147
-
148
- """
149
- if "SolutionGraph" in self.data:
150
- self.data["SolutionGraph"].graph.close()
151
- if "SourceGraph" in self.data:
152
- self.data["SourceGraph"].graph.close()
153
- self.data.clear()
154
-
155
- def get_transition_step(self, transitions: list[str] | None) -> list[WorkflowStepDefinition]:
156
- return [stp for stp in self.workflow_steps if stp.id in transitions and stp.enabled] if transitions else []
157
-
158
- def run_workflow_steps(self, start_step_id: str | None = None) -> str:
159
- if not start_step_id:
160
- trigger_steps = list(filter(lambda x: x.trigger, self.workflow_steps))
161
- else:
162
- trigger_steps = list(filter(lambda x: x.id == start_step_id, self.workflow_steps))
163
-
164
- if not trigger_steps:
165
- logging.error(f"Workflow {self.name} has no trigger steps or start step {start_step_id} not found")
166
- return "Workflow has no trigger steps"
167
-
168
- self.execution_log.append(
169
- WorkflowStepEvent(
170
- id=trigger_steps[0].id,
171
- state=StepExecutionStatus.SUCCESS,
172
- elapsed_time=0,
173
- timestamp=utils.get_iso8601_timestamp_now_unaware(),
174
- data=self.flow_message.payload if self.flow_message else None,
175
- )
176
- )
177
-
178
- step: WorkflowStepDefinition = trigger_steps[0]
179
- transition_steps = self.get_transition_step(step.transition_to)
180
-
181
- if len(transition_steps) == 0:
182
- logging.error(f"Workflow {self.name} has no transition steps from step {step.id}")
183
- return "Workflow has no transition steps"
184
-
185
- step = transition_steps[0]
186
-
187
- for _ in range(
188
- 1000
189
- ): # Max 1000 steps in a workflow is a reasonable limit and protection against infinite loops
190
- new_flow_msg = None
191
- if step.enabled:
192
- new_flow_msg = self.run_step(step)
193
- else:
194
- logging.info(f"Skipping step workflow step {step.id}")
195
- self.execution_log.append(
196
- WorkflowStepEvent(
197
- id=step.id,
198
- system_component_id=step.system_component_id,
199
- state=StepExecutionStatus.SKIPPED,
200
- elapsed_time=0,
201
- timestamp=utils.get_iso8601_timestamp_now_unaware(),
202
- error="",
203
- )
204
- )
205
-
206
- steps = self.get_transition_step(step.transition_to)
207
-
208
- if new_flow_msg:
209
- self.flow_message = new_flow_msg
210
- # If the step returned a new flow message, use it to mutate execution flow
211
- if new_flow_msg.next_step_ids:
212
- steps = self.get_transition_step(new_flow_msg.next_step_ids)
213
-
214
- if len(steps) == 0:
215
- break
216
- step = steps[0]
217
- logging.debug(f"Transitioning to step {step.id}")
218
-
219
- return "ok"
220
-
221
- def configure(self, config: Config):
222
- raise NotImplementedError()
223
-
224
- def copy(self) -> "BaseWorkflow":
225
- """Create a copy of the workflow"""
226
- new_instance = self.__class__(self.name, self.cdf_client, self.steps_registry)
227
- new_instance.workflow_steps = self.workflow_steps
228
- new_instance.configs = self.configs
229
- new_instance.set_task_builder(self.task_builder)
230
- new_instance.set_default_dataset_id(self.default_dataset_id)
231
- new_instance.set_storage_path("transformation_rules", self.rules_storage_path)
232
- if self.data_store_path:
233
- new_instance.set_storage_path("data_store", self.data_store_path)
234
- return new_instance
235
-
236
- def run_step(self, step: WorkflowStepDefinition) -> FlowMessage | None:
237
- step_name = step.id
238
- system_component_id = step.system_component_id
239
-
240
- steps_metrics.labels(wf_name=self.name, step_name=step_name, name="step_started_counter").inc()
241
- flow_message = self.flow_message
242
- if self.state != WorkflowState.RUNNING:
243
- logging.error(f"Workflow {self.name} is not running , step {step_name} is skipped")
244
- return None
245
-
246
- logging.info(f"Running step {step_name}")
247
- self.current_step = step_name
248
- start_time = time.perf_counter()
249
- stop_time = start_time
250
- step_execution_status = StepExecutionStatus.STARTED
251
- self.execution_log.append(
252
- WorkflowStepEvent(
253
- id=step_name,
254
- system_component_id=system_component_id,
255
- state=step_execution_status,
256
- elapsed_time=0,
257
- timestamp=utils.get_iso8601_timestamp_now_unaware(),
258
- error="",
259
- )
260
- )
261
- output_text = ""
262
- error_text = ""
263
- new_flow_message = None
264
- try:
265
- if step.stype == StepType.PYSTEP:
266
- # Most likely will be discontinued in the future in favor of std steps
267
- if step.method and hasattr(self, step.method):
268
- method = getattr(self, step.method)
269
- else:
270
- method_name = f"step_{step.id}"
271
- if hasattr(self, method_name):
272
- method = getattr(self, method_name)
273
- else:
274
- logging.error(f"Workflow step {step.id} has no method {method_name}")
275
- raise Exception(f"Workflow step {step.id} has no method {method_name}")
276
-
277
- @retry_decorator(
278
- max_retries=step.max_retries,
279
- retry_delay=step.retry_delay,
280
- component_name=f"wf step runner , step.id = {step.id}",
281
- )
282
- def method_runner():
283
- return method(flow_message)
284
-
285
- new_flow_message = method_runner()
286
- elif step.stype == StepType.STD_STEP:
287
- if self.steps_registry is None:
288
- raise Exception(
289
- f"Workflow step {step.id} can't be executed.Step registry is not configured or \
290
- not set as parameter in BaseWorkflow constructor"
291
- )
292
-
293
- @retry_decorator(
294
- max_retries=step.max_retries,
295
- retry_delay=step.retry_delay,
296
- component_name=f"wf step runner , step.id = {step.id}",
297
- )
298
- def method_runner():
299
- return self.steps_registry.run_step(
300
- step.method,
301
- self.data,
302
- metrics=self.metrics,
303
- workflow_configs=self.get_configs(),
304
- step_configs=step.configs,
305
- workflow_id=self.name,
306
- workflow_run_id=self.run_id,
307
- step_complex_configs=step.complex_configs,
308
- )
309
-
310
- output = method_runner()
311
- if output is not None:
312
- outputs = output if isinstance(output, tuple) else (output,)
313
- for out_obj in outputs:
314
- if isinstance(out_obj, FlowMessage):
315
- new_flow_message = out_obj
316
- elif isinstance(out_obj, DataContract):
317
- self.data[type(out_obj).__name__] = out_obj
318
- else:
319
- raise WorkflowStepOutputError(step_type=type(out_obj).__name__)
320
-
321
- elif step.stype == StepType.START_WORKFLOW_TASK_STEP:
322
- if self.task_builder:
323
- sync_str = step.params.get("sync", "false")
324
- sync = sync_str.lower() == "true" or sync_str == "1"
325
- start_status = self.task_builder.start_workflow_task(
326
- workflow_name=step.params.get("workflow_name", ""), sync=sync, flow_message=self.flow_message
327
- )
328
- if start_status.is_success and start_status.workflow_instance.state == WorkflowState.COMPLETED:
329
- # It only works with sync = true
330
- new_flow_message = start_status.workflow_instance.flow_message
331
- else:
332
- logging.error(f"Workflow step {step.id} failed to start workflow task")
333
- if start_status.is_success:
334
- raise WorkflowStartException(start_status.workflow_instance.last_error)
335
- else:
336
- raise WorkflowStartException(start_status.status_text)
337
-
338
- else:
339
- logging.error(f"Workflow step {step.id} has no task builder")
340
- raise Exception(f"Workflow step {step.id} has no task builder")
341
- elif step.stype == StepType.WAIT_FOR_EVENT:
342
- # Pause workflow execution until event is received
343
- if self.state != WorkflowState.RUNNING:
344
- logging.error(f"Workflow {self.name} is not running , step {step_name} is skipped")
345
- raise Exception(f"Workflow {self.name} is not running , step {step_name} is skipped")
346
- self.state = WorkflowState.RUNNING_WAITING
347
- timeout = 3000000.0
348
- if step.params.get("wait_timeout"):
349
- timeout = float(step.params["wait_timeout"])
350
- # reporting workflow execution before waiting for event
351
- logging.info(f"Workflow {self.name} is waiting for event")
352
- self.resume_event.wait(timeout=timeout)
353
- logging.info(f"Workflow {self.name} resumed after event")
354
- self.state = WorkflowState.RUNNING
355
- self.resume_event.clear()
356
- else:
357
- logging.error(f"Workflow step {step.id} has unsupported step type {step.stype}")
358
-
359
- stop_time = time.perf_counter()
360
- elapsed_time = stop_time - start_time
361
- logging.info(f"Step {step_name} completed in {elapsed_time} seconds with ")
362
-
363
- if new_flow_message:
364
- logging.info(f"Step {self.name} completed with status {new_flow_message.step_execution_status}")
365
- error_text = new_flow_message.error_text
366
- output_text = new_flow_message.output_text
367
- if new_flow_message.step_execution_status == StepExecutionStatus.ABORT_AND_FAIL:
368
- new_flow_message.step_execution_status = StepExecutionStatus.FAILED
369
- self.state = WorkflowState.FAILED
370
-
371
- step_execution_status = (
372
- StepExecutionStatus.SUCCESS
373
- if new_flow_message.step_execution_status == StepExecutionStatus.UNKNOWN
374
- else new_flow_message.step_execution_status
375
- )
376
- else:
377
- step_execution_status = StepExecutionStatus.SUCCESS
378
- steps_metrics.labels(wf_name=self.name, step_name=step_name, name="completed_counter").inc()
379
- timing_metrics.labels(wf_name=self.name, component="step", name=step_name).set(elapsed_time)
380
- except Exception:
381
- self.state = WorkflowState.FAILED
382
- step_execution_status = StepExecutionStatus.FAILED
383
- trace = traceback.format_exc()
384
- elapsed_time = stop_time - start_time
385
- logging.error(f"Step {step_name} failed with error : {trace}")
386
- error_text = str(trace)
387
- self.last_error = error_text
388
- traceback.print_exc()
389
- steps_metrics.labels(wf_name=self.name, step_name=step_name, name="failed_counter").inc()
390
-
391
- self.execution_log.append(
392
- WorkflowStepEvent(
393
- id=step_name,
394
- system_component_id=system_component_id,
395
- state=step_execution_status,
396
- elapsed_time=round(elapsed_time, 3),
397
- timestamp=utils.get_iso8601_timestamp_now_unaware(),
398
- error=error_text,
399
- output_text=output_text,
400
- data=new_flow_message.payload if new_flow_message else None,
401
- )
402
- )
403
- if new_flow_message:
404
- self.flow_message = new_flow_message
405
-
406
- self.report_step_execution()
407
- return new_flow_message
408
-
409
- def resume_workflow(self, flow_message: FlowMessage, step_id: str):
410
- if step_id and self.current_step != step_id:
411
- logging.error(f"Workflow {self.name} is not in step {step_id} , resume is skipped")
412
- return
413
- self.flow_message = flow_message
414
- self.execution_log.append(
415
- WorkflowStepEvent(
416
- id=step_id,
417
- state=StepExecutionStatus.SUCCESS,
418
- elapsed_time=0,
419
- timestamp=utils.get_iso8601_timestamp_now_unaware(),
420
- data=self.flow_message.payload,
421
- )
422
- )
423
- self.resume_event.set()
424
-
425
- def report_step_execution(self):
426
- pass
427
-
428
- def report_workflow_execution(self):
429
- reporting_type = "all_enabled"
430
- if config_item := self.get_config_item("system.execution_reporting_type"):
431
- reporting_type = config_item.value
432
- if reporting_type == "all_disabled":
433
- return
434
- if self.cdf_store:
435
- logging.debug("Reporting workflow execution to CDF")
436
- try:
437
- self.cdf_store.report_workflow_execution_to_cdf(self.get_state())
438
- except Exception:
439
- logging.error("Failed to report workflow execution to CDF")
440
- traceback.print_exc()
441
-
442
- def get_state(self):
443
- return WorkflowFullStateReport(
444
- workflow_name=self.name,
445
- state=self.state,
446
- run_id=self.run_id,
447
- start_time=self.start_time,
448
- end_time=self.end_time,
449
- elapsed_time=round(self.elapsed_time, 3),
450
- last_error=self.last_error,
451
- execution_log=self.execution_log,
452
- )
453
-
454
- def set_cognite_client(self, client: CogniteClient):
455
- self.cdf_client = client
456
-
457
- def get_workflow_definition(self):
458
- return WorkflowDefinition(
459
- name=self.name,
460
- steps=self.workflow_steps,
461
- system_components=self.workflow_system_components,
462
- configs=self.configs,
463
- )
464
-
465
- def add_step(self, step: WorkflowStepDefinition):
466
- self.workflow_steps.append(step)
467
-
468
- def enable_step(self, step_id: str, enabled: bool = True):
469
- """Enable or disable step in workflow. Primarily used for tests"""
470
- for step in self.workflow_steps:
471
- if step.id == step_id:
472
- step.enabled = enabled
473
-
474
- def add_system_component(self, system_components: WorkflowSystemComponent):
475
- self.workflow_system_components.append(system_components)
476
-
477
- def serialize_workflow(self, output_format: str = "json", custom_implementation_module: str | None = None) -> str:
478
- workflow_definitions = WorkflowDefinition(
479
- name=self.name,
480
- steps=self.workflow_steps,
481
- system_components=self.workflow_system_components,
482
- configs=self.configs,
483
- implementation_module=custom_implementation_module,
484
- )
485
- if output_format == "json":
486
- return json.dumps(workflow_definitions.model_dump(), indent=4)
487
- elif output_format == "yaml":
488
- return yaml.dump(workflow_definitions.model_dump(), indent=4)
489
- else:
490
- raise NotImplementedError(f"Output format {output_format} is not supported.")
491
-
492
- @classmethod
493
- def deserialize_definition(cls, json_string: str, output_format: str = "json") -> WorkflowDefinition:
494
- if output_format == "json":
495
- workflow_definitions = WorkflowDefinition.model_validate(json.loads(json_string))
496
- elif output_format == "yaml":
497
- workflow_definitions = WorkflowDefinition.model_validate(yaml.load(json_string, Loader=yaml.Loader))
498
- else:
499
- raise NotImplementedError(f"Output format {output_format} is not supported.")
500
- return workflow_definitions
501
-
502
- def set_definition(self, workflow_definition: WorkflowDefinition):
503
- self.workflow_steps = workflow_definition.steps
504
- self.workflow_system_components = workflow_definition.system_components
505
- self.configs = workflow_definition.configs
506
-
507
- def set_storage_path(self, storage_type: str, storage_path: str | Path | None) -> None:
508
- if storage_path is None:
509
- return None
510
- if storage_type == "transformation_rules":
511
- self.rules_storage_path = Path(storage_path)
512
- if self.default_dataset_id is None:
513
- raise WorkflowConfigurationNotSetError("default_dataset_id")
514
- self.cdf_store = cdf_store.CdfStore(
515
- self.cdf_client, data_set_id=self.default_dataset_id, rules_storage_path=self.rules_storage_path
516
- )
517
- elif storage_type == "data_store":
518
- self.data_store_path = Path(storage_path)
519
- self.user_steps_path = Path(storage_path, "steps")
520
-
521
- def set_task_builder(self, task_builder: WorkflowTaskBuilder | None):
522
- self.task_builder = task_builder
523
-
524
- def get_config_item(self, config_name: str) -> WorkflowConfigItem | None:
525
- return next((item for item in self.configs if item.name == config_name), None)
526
-
527
- def get_config_group_values_by_name(
528
- self, group_name: str, remove_group_prefix: bool = True
529
- ) -> dict[str, str | None]:
530
- return {
531
- (item.name.removeprefix(item.group) if remove_group_prefix else item.name): item.value
532
- for item in self.configs
533
- if item.group == group_name
534
- }
535
-
536
- def get_config_item_value(self, config_name: str, default_value=None) -> str | None:
537
- return config.value if (config := self.get_config_item(config_name)) else default_value
538
-
539
- def set_default_dataset_id(self, default_dataset_id: int | None):
540
- self.default_dataset_id = default_dataset_id
541
- self.cdf_store = (
542
- cdf_store.CdfStore(self.cdf_client, data_set_id=self.default_dataset_id)
543
- if self.default_dataset_id
544
- else None
545
- )
546
-
547
- def set_cdf_client_config(self, cdf_client_config: ClientConfig):
548
- """Set the CogniteClient configuration to be used by the workflow to create new CogniteClient instances."""
549
- self.cdf_client_config = cdf_client_config
550
-
551
- def get_new_cdf_client(self):
552
- """Get a new CogniteClient instance with the same configuration as the one used by the workflow .
553
- Should be used from workflow steps to avoid sharing the same client instance between steps or
554
- reset reference to old client instance.
555
- Returns: CogniteClient
556
- """
557
- return CogniteClient(self.cdf_client_config)
558
-
559
- def get_step_by_id(self, step_id: str) -> WorkflowStepDefinition | None:
560
- return next((step for step in self.workflow_steps if step.id == step_id), None)
561
-
562
- def get_trigger_step(self, step_id: str | None = None) -> WorkflowStepDefinition | None:
563
- if step_id:
564
- return next((step for step in self.workflow_steps if step.id == step_id and step.enabled), None)
565
- else:
566
- return next((step for step in self.workflow_steps if step.trigger and step.enabled), None)
567
-
568
- def get_context(self) -> dict[str, DataContract | FlowMessage | CdfStore | CogniteClient | None]:
569
- return self.data
570
-
571
- def get_configs(self) -> WorkflowConfigs:
572
- return WorkflowConfigs(configs=self.configs)
573
-
574
- def get_list_of_workflow_artifacts(self) -> list[Path]:
575
- # create a list of all files under the data store path
576
-
577
- file_list: list[Path] = []
578
- if self.data_store_path is None:
579
- raise WorkflowConfigurationNotSetError("data_store_path")
580
- workflow_data_path = Path(self.data_store_path) / "workflows" / self.name
581
- try:
582
- for root, _dirs, files in os.walk(workflow_data_path):
583
- for file in files:
584
- full_path = Path(root) / file
585
- file_list.append(Path(full_path).relative_to(workflow_data_path))
586
- return file_list
587
- except OSError as e:
588
- # Handle exceptions like directory not found, permission denied, etc.
589
- logging.error(f"Error while listing workflow artifacts for {self.name} : {e}")
590
- return []