runnable 0.1.0__py3-none-any.whl → 0.2.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 (71) hide show
  1. runnable/__init__.py +34 -0
  2. runnable/catalog.py +141 -0
  3. runnable/cli.py +272 -0
  4. runnable/context.py +34 -0
  5. runnable/datastore.py +686 -0
  6. runnable/defaults.py +179 -0
  7. runnable/entrypoints.py +484 -0
  8. runnable/exceptions.py +94 -0
  9. runnable/executor.py +431 -0
  10. runnable/experiment_tracker.py +139 -0
  11. runnable/extensions/catalog/__init__.py +21 -0
  12. runnable/extensions/catalog/file_system/__init__.py +0 -0
  13. runnable/extensions/catalog/file_system/implementation.py +226 -0
  14. runnable/extensions/catalog/k8s_pvc/__init__.py +0 -0
  15. runnable/extensions/catalog/k8s_pvc/implementation.py +16 -0
  16. runnable/extensions/catalog/k8s_pvc/integration.py +59 -0
  17. runnable/extensions/executor/__init__.py +714 -0
  18. runnable/extensions/executor/argo/__init__.py +0 -0
  19. runnable/extensions/executor/argo/implementation.py +1182 -0
  20. runnable/extensions/executor/argo/specification.yaml +51 -0
  21. runnable/extensions/executor/k8s_job/__init__.py +0 -0
  22. runnable/extensions/executor/k8s_job/implementation_FF.py +259 -0
  23. runnable/extensions/executor/k8s_job/integration_FF.py +69 -0
  24. runnable/extensions/executor/local/__init__.py +0 -0
  25. runnable/extensions/executor/local/implementation.py +69 -0
  26. runnable/extensions/executor/local_container/__init__.py +0 -0
  27. runnable/extensions/executor/local_container/implementation.py +367 -0
  28. runnable/extensions/executor/mocked/__init__.py +0 -0
  29. runnable/extensions/executor/mocked/implementation.py +220 -0
  30. runnable/extensions/experiment_tracker/__init__.py +0 -0
  31. runnable/extensions/experiment_tracker/mlflow/__init__.py +0 -0
  32. runnable/extensions/experiment_tracker/mlflow/implementation.py +94 -0
  33. runnable/extensions/nodes.py +675 -0
  34. runnable/extensions/run_log_store/__init__.py +0 -0
  35. runnable/extensions/run_log_store/chunked_file_system/__init__.py +0 -0
  36. runnable/extensions/run_log_store/chunked_file_system/implementation.py +106 -0
  37. runnable/extensions/run_log_store/chunked_k8s_pvc/__init__.py +0 -0
  38. runnable/extensions/run_log_store/chunked_k8s_pvc/implementation.py +21 -0
  39. runnable/extensions/run_log_store/chunked_k8s_pvc/integration.py +61 -0
  40. runnable/extensions/run_log_store/db/implementation_FF.py +157 -0
  41. runnable/extensions/run_log_store/db/integration_FF.py +0 -0
  42. runnable/extensions/run_log_store/file_system/__init__.py +0 -0
  43. runnable/extensions/run_log_store/file_system/implementation.py +136 -0
  44. runnable/extensions/run_log_store/generic_chunked.py +541 -0
  45. runnable/extensions/run_log_store/k8s_pvc/__init__.py +0 -0
  46. runnable/extensions/run_log_store/k8s_pvc/implementation.py +21 -0
  47. runnable/extensions/run_log_store/k8s_pvc/integration.py +56 -0
  48. runnable/extensions/secrets/__init__.py +0 -0
  49. runnable/extensions/secrets/dotenv/__init__.py +0 -0
  50. runnable/extensions/secrets/dotenv/implementation.py +100 -0
  51. runnable/extensions/secrets/env_secrets/__init__.py +0 -0
  52. runnable/extensions/secrets/env_secrets/implementation.py +42 -0
  53. runnable/graph.py +464 -0
  54. runnable/integration.py +205 -0
  55. runnable/interaction.py +399 -0
  56. runnable/names.py +546 -0
  57. runnable/nodes.py +489 -0
  58. runnable/parameters.py +183 -0
  59. runnable/pickler.py +102 -0
  60. runnable/sdk.py +470 -0
  61. runnable/secrets.py +95 -0
  62. runnable/tasks.py +392 -0
  63. runnable/utils.py +630 -0
  64. runnable-0.2.0.dist-info/METADATA +437 -0
  65. runnable-0.2.0.dist-info/RECORD +69 -0
  66. runnable-0.2.0.dist-info/entry_points.txt +44 -0
  67. runnable-0.1.0.dist-info/METADATA +0 -16
  68. runnable-0.1.0.dist-info/RECORD +0 -6
  69. /runnable/{.gitkeep → extensions/__init__.py} +0 -0
  70. {runnable-0.1.0.dist-info → runnable-0.2.0.dist-info}/LICENSE +0 -0
  71. {runnable-0.1.0.dist-info → runnable-0.2.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,205 @@
1
+ import logging
2
+
3
+ from stevedore import extension
4
+
5
+ from runnable import defaults
6
+ from runnable.executor import BaseExecutor
7
+
8
+ logger = logging.getLogger(defaults.LOGGER_NAME)
9
+ logging.getLogger("stevedore").setLevel(logging.CRITICAL)
10
+
11
+ # --8<-- [start:docs]
12
+
13
+
14
+ class BaseIntegration:
15
+ """
16
+ Base class for handling integration between Executor and one of Catalog, Secrets, RunLogStore.
17
+ """
18
+
19
+ executor_type = ""
20
+ service_type = "" # One of secret, catalog, datastore, experiment tracker
21
+ service_provider = "" # The actual implementation of the service
22
+
23
+ def __init__(self, executor: "BaseExecutor", integration_service: object):
24
+ self.executor = executor
25
+ self.service = integration_service
26
+
27
+ def validate(self, **kwargs):
28
+ """
29
+ Raise an exception if the executor_type is not compatible with service provider.
30
+
31
+ By default, it is considered as compatible.
32
+ """
33
+
34
+ def configure_for_traversal(self, **kwargs):
35
+ """
36
+ Do any changes needed to both executor and service provider during traversal of the graph.
37
+
38
+ By default, no change is required.
39
+ """
40
+
41
+ def configure_for_execution(self, **kwargs):
42
+ """
43
+ Do any changes needed to both executor and service provider during execution of a node.
44
+
45
+ By default, no change is required.
46
+ """
47
+
48
+
49
+ # --8<-- [end:docs]
50
+
51
+
52
+ def get_integration_handler(executor: "BaseExecutor", service: object) -> BaseIntegration:
53
+ """
54
+ Return the integration handler between executor and the service.
55
+
56
+ If none found to be implemented, return the BaseIntegration which does nothing.
57
+
58
+ Args:
59
+ executor (BaseExecutor): The executor
60
+ service (object): The service provider
61
+
62
+ Returns:
63
+ [BaseIntegration]: The implemented integration handler or BaseIntegration if none found
64
+
65
+ Raises:
66
+ Exception: If multiple integrations are found for the executor and service
67
+ """
68
+ service_type = service.service_type # type: ignore
69
+ service_name = getattr(service, "service_name")
70
+ integrations = []
71
+
72
+ # Get all the integrations defined by the 3rd party in their pyproject.toml
73
+ mgr = extension.ExtensionManager(
74
+ namespace="integration",
75
+ invoke_on_load=True,
76
+ invoke_kwds={"executor": executor, "integration_service": service},
77
+ )
78
+ for _, kls in mgr.items():
79
+ if (
80
+ kls.obj.executor_type == executor.service_name
81
+ and kls.obj.service_type == service_type
82
+ and kls.obj.service_provider == service_name
83
+ ):
84
+ logger.info(f"Identified an integration pattern {kls.obj}")
85
+ integrations.append(kls.obj)
86
+
87
+ # Get all the implementations defined by the magnus package
88
+ for kls in BaseIntegration.__subclasses__():
89
+ # Match the exact service type
90
+ if kls.service_type == service_type and kls.service_provider == service_name:
91
+ # Match either all executor or specific ones provided
92
+ if kls.executor_type == "" or kls.executor_type == executor.service_name:
93
+ integrations.append(kls(executor=executor, integration_service=service))
94
+
95
+ if len(integrations) > 1:
96
+ msg = (
97
+ f"Multiple integrations between {executor.service_name} and {service_name} of type {service_type} found. "
98
+ "If you defined an integration pattern, please ensure it is specific and does not conflict with magnus "
99
+ " implementations."
100
+ )
101
+ logger.exception(msg)
102
+ raise Exception(msg)
103
+
104
+ if not integrations:
105
+ logger.warning(
106
+ f"Could not find an integration pattern for {executor.service_name} and {service_name} for {service_type}."
107
+ " This implies that there is no need to change the configurations."
108
+ )
109
+ return BaseIntegration(executor, service)
110
+
111
+ return integrations[0]
112
+
113
+
114
+ def validate(executor: "BaseExecutor", service: object, **kwargs):
115
+ """
116
+ Helper function to resolve the Integration class and validate the compatibility between executor and service
117
+
118
+ Args:
119
+ executor (BaseExecutor) : The executor
120
+ service (object): The service provider
121
+ """
122
+ integration_handler = get_integration_handler(executor, service)
123
+ integration_handler.validate(**kwargs)
124
+
125
+
126
+ def configure_for_traversal(executor: "BaseExecutor", service: object, **kwargs):
127
+ """
128
+ Helper function to resolve the Integration class and configure the executor and service for graph traversal
129
+
130
+ Args:
131
+ executor (BaseExecutor) : The executor
132
+ service (object): The service provider
133
+ """
134
+ integration_handler = get_integration_handler(executor, service)
135
+ integration_handler.configure_for_traversal(**kwargs)
136
+
137
+
138
+ def configure_for_execution(executor: "BaseExecutor", service: object, **kwargs):
139
+ """
140
+ Helper function to resolve the Integration class and configure the executor and service for execution
141
+
142
+ Args:
143
+ executor (BaseExecutor) : The executor
144
+ service (object): The service provider
145
+ """
146
+ integration_handler = get_integration_handler(executor, service)
147
+ integration_handler.configure_for_execution(**kwargs)
148
+
149
+
150
+ class BufferedRunLogStore(BaseIntegration):
151
+ """
152
+ Integration between any executor and buffered run log store
153
+ """
154
+
155
+ service_type = "run_log_store" # One of secret, catalog, datastore
156
+ service_provider = "buffered" # The actual implementation of the service
157
+
158
+ def validate(self, **kwargs):
159
+ if not self.executor.service_name == "local":
160
+ raise Exception("Buffered run log store is only supported for local executor")
161
+
162
+ msg = (
163
+ "Run log generated by buffered run log store are not persisted. "
164
+ "Re-running this run, in case of a failure, is not possible"
165
+ )
166
+ logger.warning(msg)
167
+
168
+
169
+ class DoNothingCatalog(BaseIntegration):
170
+ """
171
+ Integration between any executor and do nothing catalog
172
+ """
173
+
174
+ service_type = "catalog" # One of secret, catalog, datastore
175
+ service_provider = "do-nothing" # The actual implementation of the service
176
+
177
+ def validate(self, **kwargs):
178
+ msg = "A do-nothing catalog does not hold any data and therefore cannot pass data between nodes."
179
+ logger.warning(msg)
180
+
181
+
182
+ class DoNothingSecrets(BaseIntegration):
183
+ """
184
+ Integration between any executor and do nothing secrets
185
+ """
186
+
187
+ service_type = "secrets" # One of secret, catalog, datastore
188
+ service_provider = "do-nothing" # The actual implementation of the service
189
+
190
+ def validate(self, **kwargs):
191
+ msg = "A do-nothing secrets does not hold any secrets and therefore cannot return you any secrets."
192
+ logger.warning(msg)
193
+
194
+
195
+ class DoNothingExperimentTracker(BaseIntegration):
196
+ """
197
+ Integration between any executor and do nothing experiment tracker
198
+ """
199
+
200
+ service_type = "experiment_tracker" # One of secret, catalog, datastore
201
+ service_provider = "do-nothing" # The actual implementation of the service
202
+
203
+ def validate(self, **kwargs):
204
+ msg = "A do-nothing experiment tracker does nothing and therefore cannot track anything."
205
+ logger.warning(msg)
@@ -0,0 +1,399 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import os
6
+ from functools import wraps
7
+ from typing import Any, ContextManager, Dict, Optional, TypeVar, Union, cast, overload
8
+
9
+ from pydantic import BaseModel
10
+
11
+ import runnable.context as context
12
+ from runnable import defaults, exceptions, parameters, pickler, utils
13
+ from runnable.datastore import RunLog, StepLog
14
+
15
+ logger = logging.getLogger(defaults.LOGGER_NAME)
16
+
17
+ CastT = TypeVar("CastT")
18
+
19
+
20
+ def check_context(func):
21
+ @wraps(func)
22
+ def wrapper(*args, **kwargs):
23
+ if not context.run_context.executor:
24
+ msg = (
25
+ "There are no active executor and services. This should not have happened and is a bug."
26
+ " Please raise a bug report."
27
+ )
28
+ raise Exception(msg)
29
+ result = func(*args, **kwargs)
30
+ return result
31
+
32
+ return wrapper
33
+
34
+
35
+ @check_context
36
+ def track_this(step: int = 0, **kwargs):
37
+ """
38
+ Tracks key-value pairs to the experiment tracker.
39
+
40
+ The value is dumped as a dict, by alias, if it is a pydantic model.
41
+
42
+ Args:
43
+ step (int, optional): The step to track the data at. Defaults to 0.
44
+ **kwargs (dict): The key-value pairs to track.
45
+
46
+ Examples:
47
+ >>> track_this(step=0, my_int_param=123, my_float_param=123.45, my_str_param='hello world')
48
+ >>> track_this(step=1, my_int_param=456, my_float_param=456.78, my_str_param='goodbye world')
49
+ """
50
+ prefix = defaults.TRACK_PREFIX
51
+
52
+ for key, value in kwargs.items():
53
+ logger.info(f"Tracking {key} with value: {value}")
54
+
55
+ if isinstance(value, BaseModel):
56
+ value = value.model_dump(by_alias=True)
57
+
58
+ os.environ[prefix + key + f"{defaults.STEP_INDICATOR}{step}"] = json.dumps(value)
59
+
60
+
61
+ @check_context
62
+ def set_parameter(**kwargs) -> None:
63
+ """
64
+ Store a set of parameters.
65
+
66
+ !!! note
67
+ The parameters are not stored in run log at this point in time.
68
+ They are collected now and stored in the run log after completion of the task.
69
+
70
+ Parameters:
71
+ **kwargs (dict): A dictionary of key-value pairs to store as parameters.
72
+
73
+ Returns:
74
+ None
75
+
76
+ Examples:
77
+ >>> set_parameter(my_int_param=123, my_float_param=123.45, my_bool_param=True, my_str_param='hello world')
78
+ >>> get_parameter('my_int_param', int)
79
+ 123
80
+ >>> get_parameter('my_float_param', float)
81
+ 123.45
82
+ >>> get_parameter('my_bool_param', bool)
83
+ True
84
+ >>> get_parameter('my_str_param', str)
85
+ 'hello world'
86
+
87
+ >>> # Example of using Pydantic models
88
+ >>> class MyModel(BaseModel):
89
+ ... field1: str
90
+ ... field2: int
91
+ >>> set_parameter(my_model_param=MyModel(field1='value1', field2=2))
92
+ >>> get_parameter('my_model_param', MyModel)
93
+ MyModel(field1='value1', field2=2)
94
+
95
+ """
96
+ parameters.set_user_defined_params_as_environment_variables(kwargs)
97
+
98
+
99
+ @overload
100
+ def get_parameter(key: str, cast_as: Optional[CastT]) -> CastT:
101
+ ...
102
+
103
+
104
+ @overload
105
+ def get_parameter(cast_as: Optional[CastT]) -> CastT:
106
+ ...
107
+
108
+
109
+ @check_context
110
+ def get_parameter(key: Optional[str] = None, cast_as: Optional[CastT] = None) -> Union[Dict[str, Any], CastT]:
111
+ """
112
+ Get a parameter by its key.
113
+ If the key is not provided, all parameters will be returned.
114
+
115
+ cast_as is not required for JSON supported type (int, float, bool, str).
116
+ For complex nested parameters, cast_as could package them into a pydantic model.
117
+ If cast_as is not provided, the type will remain as dict for nested structures.
118
+
119
+ Note that the cast_as pydantic model is the class, not an instance.
120
+
121
+ Args:
122
+ key (str, optional): The key of the parameter to retrieve. If not provided, all parameters will be returned.
123
+ cast_as (Type, optional): The type to cast the parameter to. If not provided, the type will remain as it is
124
+ for simple data types (int, float, bool, str). For nested parameters, it would be a dict.
125
+
126
+ Raises:
127
+ Exception: If the parameter does not exist and key is not provided.
128
+ ValidationError: If the parameter cannot be cast as pydantic model, when cast_as is provided.
129
+
130
+ Examples:
131
+ >>> get_parameter('my_int_param', int)
132
+ 123
133
+ >>> get_parameter('my_float_param', float)
134
+ 123.45
135
+ >>> get_parameter('my_bool_param', bool)
136
+ True
137
+ >>> get_parameter('my_str_param', str)
138
+ 'hello world'
139
+ >>> get_parameter('my_model_param', MyModel)
140
+ MyModel(field1='value1', field2=2)
141
+ >>> get_parameter(cast_as=MyModel)
142
+ MyModel(field1='value1', field2=2)
143
+
144
+ """
145
+ params = parameters.get_user_set_parameters(remove=False)
146
+
147
+ if not key:
148
+ # Return all parameters
149
+ return cast(CastT, parameters.cast_parameters_as_type(params, cast_as)) # type: ignore
150
+
151
+ if key not in params:
152
+ raise Exception(f"Parameter {key} is not set before")
153
+
154
+ # Return the parameter value, casted as asked.
155
+ return cast(CastT, parameters.cast_parameters_as_type(params[key], cast_as)) # type: ignore
156
+
157
+
158
+ @check_context
159
+ def get_secret(secret_name: str) -> str:
160
+ """
161
+ Retrieve a secret from the secret store.
162
+
163
+ Args:
164
+ secret_name (str): The name of the secret to retrieve.
165
+
166
+ Raises:
167
+ SecretNotFoundError: If the secret does not exist in the store.
168
+
169
+ Returns:
170
+ str: The secret value.
171
+ """
172
+ secrets_handler = context.run_context.secrets_handler
173
+ try:
174
+ return secrets_handler.get(name=secret_name)
175
+ except exceptions.SecretNotFoundError:
176
+ logger.exception(f"No secret by the name {secret_name} found in the store")
177
+ raise
178
+
179
+
180
+ @check_context
181
+ def get_from_catalog(name: str, destination_folder: str = ""):
182
+ """
183
+ Get data from the catalog.
184
+
185
+ The name can be a wildcard pattern following globing rules.
186
+
187
+ Args:
188
+ name (str): The name of the data catalog entry.
189
+ destination_folder (str, optional): The destination folder to download the data to.
190
+ If not provided, the default destination folder set in the catalog will be used.
191
+ """
192
+ if not destination_folder:
193
+ destination_folder = context.run_context.catalog_handler.compute_data_folder
194
+
195
+ data_catalog = context.run_context.catalog_handler.get(
196
+ name,
197
+ run_id=context.run_context.run_id,
198
+ )
199
+
200
+ if context.run_context.executor._context_step_log:
201
+ context.run_context.executor._context_step_log.add_data_catalogs(data_catalog)
202
+ else:
203
+ logger.warning("Step log context was not found during interaction! The step log will miss the record")
204
+
205
+
206
+ @check_context
207
+ def put_in_catalog(filepath: str):
208
+ """
209
+ Add a file or folder to the data catalog.
210
+ You can use wild cards following globing rules.
211
+
212
+ Args:
213
+ filepath (str): The path to the file or folder added to the catalog
214
+ """
215
+
216
+ data_catalog = context.run_context.catalog_handler.put(
217
+ filepath,
218
+ run_id=context.run_context.run_id,
219
+ )
220
+ if not data_catalog:
221
+ logger.warning(f"No catalog was done by the {filepath}")
222
+
223
+ if context.run_context.executor._context_step_log:
224
+ context.run_context.executor._context_step_log.add_data_catalogs(data_catalog)
225
+ else:
226
+ logger.warning("Step log context was not found during interaction! The step log will miss the record")
227
+
228
+
229
+ @check_context
230
+ def put_object(data: Any, name: str):
231
+ """
232
+ Serialize and store a python object in the data catalog.
233
+
234
+ This function behaves the same as `put_in_catalog`
235
+ but with python objects.
236
+
237
+ Args:
238
+ data (Any): The python data object to store.
239
+ name (str): The name to store it against.
240
+ """
241
+ native_pickler = pickler.NativePickler()
242
+
243
+ native_pickler.dump(data=data, path=name)
244
+ put_in_catalog(f"{name}{native_pickler.extension}")
245
+
246
+ # Remove the file
247
+ os.remove(f"{name}{native_pickler.extension}")
248
+
249
+
250
+ @check_context
251
+ def get_object(name: str) -> Any:
252
+ """
253
+ Retrieve and deserialize a python object from the data catalog.
254
+
255
+ This function behaves the same as `get_from_catalog` but with
256
+ python objects.
257
+
258
+ Returns:
259
+ Any : The object
260
+ """
261
+ native_pickler = pickler.NativePickler()
262
+
263
+ get_from_catalog(name=f"{name}{native_pickler.extension}", destination_folder=".")
264
+
265
+ try:
266
+ data = native_pickler.load(name)
267
+
268
+ # Remove the file
269
+ os.remove(f"{name}{native_pickler.extension}")
270
+ return data
271
+ except FileNotFoundError as e:
272
+ msg = f"No object by the name {name} has been put in the catalog before."
273
+ logger.exception(msg)
274
+ raise e
275
+
276
+
277
+ @check_context
278
+ def get_run_id() -> str:
279
+ """
280
+ Returns the run_id of the current run.
281
+
282
+ You can also access this from the environment variable `MAGNUS_RUN_ID`.
283
+ """
284
+ return context.run_context.run_id
285
+
286
+
287
+ @check_context
288
+ def get_run_log() -> RunLog:
289
+ """
290
+ Returns the run_log of the current run.
291
+
292
+ The return is a deep copy of the run log to prevent any modification.
293
+ """
294
+ return context.run_context.run_log_store.get_run_log_by_id(
295
+ context.run_context.run_id,
296
+ full=True,
297
+ ).copy(deep=True)
298
+
299
+
300
+ @check_context
301
+ def get_tag() -> str:
302
+ """
303
+ Returns the tag from the environment.
304
+
305
+ Returns:
306
+ str: The tag if provided for the run, otherwise None
307
+ """
308
+ return context.run_context.tag
309
+
310
+
311
+ @check_context
312
+ def get_experiment_tracker_context() -> ContextManager:
313
+ """
314
+ Return a context session of the experiment tracker.
315
+
316
+ You can start to use the context with the python ```with``` statement.
317
+ """
318
+ experiment_tracker = context.run_context.experiment_tracker
319
+ return experiment_tracker.client_context
320
+
321
+
322
+ def start_interactive_session(run_id: str = "", config_file: str = "", tag: str = "", parameters_file: str = ""):
323
+ """
324
+ During interactive python coding, either via notebooks or ipython, you can start a magnus session by calling
325
+ this function. The executor would always be local executor as its interactive.
326
+
327
+ If this was called during a pipeline/function/notebook execution, it will be ignored.
328
+
329
+ Args:
330
+ run_id (str, optional): The run id to use. Defaults to "" and would be created if not provided.
331
+ config_file (str, optional): The configuration file to use. Defaults to "" and magnus defaults.
332
+ tag (str, optional): The tag to attach to the run. Defaults to "".
333
+ parameters_file (str, optional): The parameters file to use. Defaults to "".
334
+ """
335
+
336
+ from runnable import entrypoints, graph # pylint: disable=import-outside-toplevel
337
+
338
+ if context.run_context.executor:
339
+ logger.warn("This is not an interactive session or a session has already been activated.")
340
+ return
341
+
342
+ run_id = utils.generate_run_id(run_id=run_id)
343
+ context.run_context = entrypoints.prepare_configurations(
344
+ configuration_file=config_file,
345
+ run_id=run_id,
346
+ tag=tag,
347
+ parameters_file=parameters_file,
348
+ force_local_executor=True,
349
+ )
350
+
351
+ executor = context.run_context.executor
352
+
353
+ utils.set_magnus_environment_variables(run_id=run_id, configuration_file=config_file, tag=tag)
354
+
355
+ context.run_context.execution_plan = defaults.EXECUTION_PLAN.INTERACTIVE.value
356
+ executor.prepare_for_graph_execution()
357
+ step_config = {
358
+ "command": "interactive",
359
+ "command_type": "python",
360
+ "type": "task",
361
+ "next": "success",
362
+ }
363
+
364
+ node = graph.create_node(name="interactive", step_config=step_config)
365
+ step_log = context.run_context.run_log_store.create_step_log("interactive", node._get_step_log_name())
366
+ executor.add_code_identities(node=node, step_log=step_log)
367
+
368
+ step_log.step_type = node.node_type
369
+ step_log.status = defaults.PROCESSING
370
+ executor._context_step_log = step_log
371
+
372
+
373
+ def end_interactive_session():
374
+ """
375
+ Ends an interactive session.
376
+
377
+ Does nothing if the executor is not interactive.
378
+ """
379
+
380
+ if not context.run_context.executor:
381
+ logger.warn("There is no active session in play, doing nothing!")
382
+ return
383
+
384
+ if context.run_context.execution_plan != defaults.EXECUTION_PLAN.INTERACTIVE.value:
385
+ logger.warn("There is not an interactive session, doing nothing!")
386
+ return
387
+
388
+ tracked_data = utils.get_tracked_data()
389
+ set_parameters = parameters.get_user_set_parameters(remove=True)
390
+
391
+ step_log = cast(StepLog, context.run_context.executor._context_step_log)
392
+ step_log.user_defined_metrics = tracked_data
393
+ context.run_context.run_log_store.add_step_log(step_log, context.run_context.run_id)
394
+
395
+ context.run_context.run_log_store.set_parameters(context.run_context.run_id, set_parameters)
396
+
397
+ context.run_context.executor._context_step_log = None
398
+ context.run_context.execution_plan = ""
399
+ context.run_context.executor = None # type: ignore