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.
- runnable/__init__.py +34 -0
- runnable/catalog.py +141 -0
- runnable/cli.py +272 -0
- runnable/context.py +34 -0
- runnable/datastore.py +686 -0
- runnable/defaults.py +179 -0
- runnable/entrypoints.py +484 -0
- runnable/exceptions.py +94 -0
- runnable/executor.py +431 -0
- runnable/experiment_tracker.py +139 -0
- runnable/extensions/catalog/__init__.py +21 -0
- runnable/extensions/catalog/file_system/__init__.py +0 -0
- runnable/extensions/catalog/file_system/implementation.py +226 -0
- runnable/extensions/catalog/k8s_pvc/__init__.py +0 -0
- runnable/extensions/catalog/k8s_pvc/implementation.py +16 -0
- runnable/extensions/catalog/k8s_pvc/integration.py +59 -0
- runnable/extensions/executor/__init__.py +714 -0
- runnable/extensions/executor/argo/__init__.py +0 -0
- runnable/extensions/executor/argo/implementation.py +1182 -0
- runnable/extensions/executor/argo/specification.yaml +51 -0
- runnable/extensions/executor/k8s_job/__init__.py +0 -0
- runnable/extensions/executor/k8s_job/implementation_FF.py +259 -0
- runnable/extensions/executor/k8s_job/integration_FF.py +69 -0
- runnable/extensions/executor/local/__init__.py +0 -0
- runnable/extensions/executor/local/implementation.py +69 -0
- runnable/extensions/executor/local_container/__init__.py +0 -0
- runnable/extensions/executor/local_container/implementation.py +367 -0
- runnable/extensions/executor/mocked/__init__.py +0 -0
- runnable/extensions/executor/mocked/implementation.py +220 -0
- runnable/extensions/experiment_tracker/__init__.py +0 -0
- runnable/extensions/experiment_tracker/mlflow/__init__.py +0 -0
- runnable/extensions/experiment_tracker/mlflow/implementation.py +94 -0
- runnable/extensions/nodes.py +675 -0
- runnable/extensions/run_log_store/__init__.py +0 -0
- runnable/extensions/run_log_store/chunked_file_system/__init__.py +0 -0
- runnable/extensions/run_log_store/chunked_file_system/implementation.py +106 -0
- runnable/extensions/run_log_store/chunked_k8s_pvc/__init__.py +0 -0
- runnable/extensions/run_log_store/chunked_k8s_pvc/implementation.py +21 -0
- runnable/extensions/run_log_store/chunked_k8s_pvc/integration.py +61 -0
- runnable/extensions/run_log_store/db/implementation_FF.py +157 -0
- runnable/extensions/run_log_store/db/integration_FF.py +0 -0
- runnable/extensions/run_log_store/file_system/__init__.py +0 -0
- runnable/extensions/run_log_store/file_system/implementation.py +136 -0
- runnable/extensions/run_log_store/generic_chunked.py +541 -0
- runnable/extensions/run_log_store/k8s_pvc/__init__.py +0 -0
- runnable/extensions/run_log_store/k8s_pvc/implementation.py +21 -0
- runnable/extensions/run_log_store/k8s_pvc/integration.py +56 -0
- runnable/extensions/secrets/__init__.py +0 -0
- runnable/extensions/secrets/dotenv/__init__.py +0 -0
- runnable/extensions/secrets/dotenv/implementation.py +100 -0
- runnable/extensions/secrets/env_secrets/__init__.py +0 -0
- runnable/extensions/secrets/env_secrets/implementation.py +42 -0
- runnable/graph.py +464 -0
- runnable/integration.py +205 -0
- runnable/interaction.py +399 -0
- runnable/names.py +546 -0
- runnable/nodes.py +489 -0
- runnable/parameters.py +183 -0
- runnable/pickler.py +102 -0
- runnable/sdk.py +470 -0
- runnable/secrets.py +95 -0
- runnable/tasks.py +392 -0
- runnable/utils.py +630 -0
- runnable-0.2.0.dist-info/METADATA +437 -0
- runnable-0.2.0.dist-info/RECORD +69 -0
- runnable-0.2.0.dist-info/entry_points.txt +44 -0
- runnable-0.1.0.dist-info/METADATA +0 -16
- runnable-0.1.0.dist-info/RECORD +0 -6
- /runnable/{.gitkeep → extensions/__init__.py} +0 -0
- {runnable-0.1.0.dist-info → runnable-0.2.0.dist-info}/LICENSE +0 -0
- {runnable-0.1.0.dist-info → runnable-0.2.0.dist-info}/WHEEL +0 -0
runnable/integration.py
ADDED
@@ -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)
|
runnable/interaction.py
ADDED
@@ -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
|