simstack 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.
- simstack/__init__.py +0 -0
- simstack/_version.py +34 -0
- simstack/core/__init__.py +0 -0
- simstack/core/artifacts.py +447 -0
- simstack/core/asnyc_helper.py +14 -0
- simstack/core/config_file.py +60 -0
- simstack/core/context.py +167 -0
- simstack/core/data.py +139 -0
- simstack/core/definitions.py +21 -0
- simstack/core/engine.py +116 -0
- simstack/core/find_simstack_modules.py +43 -0
- simstack/core/hash.py +177 -0
- simstack/core/model_table.py +199 -0
- simstack/core/node.py +911 -0
- simstack/core/node_runner.py +254 -0
- simstack/core/node_table.py +380 -0
- simstack/core/recompute_artifacts.py +120 -0
- simstack/core/reset_db.py +12 -0
- simstack/core/resources.py +101 -0
- simstack/core/run_node.py +69 -0
- simstack/core/runner.py +435 -0
- simstack/core/simstack_result.py +41 -0
- simstack/core/submit_node.py +143 -0
- simstack/core/task_id.py +22 -0
- simstack/data/__init__.py +0 -0
- simstack/methods/__init__.py +0 -0
- simstack/methods/get_file.py +10 -0
- simstack/methods/multiple_executor.py +307 -0
- simstack/models/__init__.py +37 -0
- simstack/models/array_list.py +48 -0
- simstack/models/array_storage.py +54 -0
- simstack/models/artifact_models.py +127 -0
- simstack/models/base_types.py +208 -0
- simstack/models/charts_artifact.py +411 -0
- simstack/models/dataset.py +432 -0
- simstack/models/dataset_metadata.py +303 -0
- simstack/models/file_instance.py +77 -0
- simstack/models/file_list.py +321 -0
- simstack/models/files.py +229 -0
- simstack/models/log_entry_model.py +37 -0
- simstack/models/models.py +40 -0
- simstack/models/node_registry.py +105 -0
- simstack/models/pandas_model.py +210 -0
- simstack/models/parameters.py +388 -0
- simstack/models/pickle_models.py +311 -0
- simstack/models/runner_model.py +39 -0
- simstack/models/simple_table.py +32 -0
- simstack/models/simstack_model.py +97 -0
- simstack/models/slurm_info.py +17 -0
- simstack/models/table_artifact.py +87 -0
- simstack/util/__init__.py +0 -0
- simstack/util/async_zip_utils.py +18 -0
- simstack/util/cleaned_json_schema.py +20 -0
- simstack/util/config_reader.py +273 -0
- simstack/util/custom_model_dump.py +75 -0
- simstack/util/db.py +457 -0
- simstack/util/db_logger.py +117 -0
- simstack/util/default_from_dict.py +57 -0
- simstack/util/directory_iterator.py +216 -0
- simstack/util/file_hashing.py +68 -0
- simstack/util/files.py +396 -0
- simstack/util/find_python_files.py +33 -0
- simstack/util/generate_ui_schema.py +80 -0
- simstack/util/geometry_utils.py +118 -0
- simstack/util/get_module_path.py +26 -0
- simstack/util/git_pull.py +65 -0
- simstack/util/git_repository_status.py +93 -0
- simstack/util/import_module.py +65 -0
- simstack/util/importer.py +163 -0
- simstack/util/make_graph.py +107 -0
- simstack/util/make_table.py +827 -0
- simstack/util/minimal_route_finder.py +72 -0
- simstack/util/mock_db.py +144 -0
- simstack/util/module_path_checker.py +38 -0
- simstack/util/path_manager.py +234 -0
- simstack/util/project_root_finder.py +36 -0
- simstack/util/queue_watcher.py +262 -0
- simstack/util/route_finder.py +74 -0
- simstack/util/runner_util.py +162 -0
- simstack/util/safe_code_executor.py +194 -0
- simstack/util/sample_config.py +98 -0
- simstack/util/sensitive_content_filter.py +48 -0
- simstack/util/setup_logging.py +49 -0
- simstack/util/submit_to_watchdog.py +106 -0
- simstack/util/ui_tools.py +213 -0
- simstack-0.2.0.dist-info/METADATA +361 -0
- simstack-0.2.0.dist-info/RECORD +91 -0
- simstack-0.2.0.dist-info/WHEEL +5 -0
- simstack-0.2.0.dist-info/entry_points.txt +10 -0
- simstack-0.2.0.dist-info/licenses/LICENSE +21 -0
- simstack-0.2.0.dist-info/top_level.txt +1 -0
simstack/models/files.py
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
4
|
+
import zlib
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Optional, Union, Dict, Any
|
|
7
|
+
|
|
8
|
+
from odmantic import Model, Field, ObjectId
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from simstack.models import simstack_model
|
|
12
|
+
from simstack.models.file_instance import FileInstance
|
|
13
|
+
from simstack.models.parameters import Resource
|
|
14
|
+
from simstack.util.file_hashing import hash_file
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@simstack_model
|
|
20
|
+
class FileStack(Model):
|
|
21
|
+
name: Optional[str] = Field(description="Name of the file", default=None)
|
|
22
|
+
size: Optional[int] = Field(description="Size of the file in bytes", default=None)
|
|
23
|
+
is_hashable: bool = Field(
|
|
24
|
+
default=False, description="Whether the file stack is hashable"
|
|
25
|
+
)
|
|
26
|
+
hash: Optional[str] = Field(
|
|
27
|
+
default=None, description="Hash of the file stack content", index=True
|
|
28
|
+
)
|
|
29
|
+
in_memory: bool = Field(
|
|
30
|
+
default=False, description="Whether the file stack is in memory"
|
|
31
|
+
)
|
|
32
|
+
content: Optional[bytes] = Field(
|
|
33
|
+
description="Compressed file content", default=None
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
locations: List[FileInstance] = Field(
|
|
37
|
+
default_factory=list, description="List of file locations"
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def __str__(self):
|
|
41
|
+
return f"FileStack(name={self.name}, size={self.size}, is_hashable={self.is_hashable}, in_memory={self.in_memory}, locations={self.locations})"
|
|
42
|
+
|
|
43
|
+
def __repr__(self):
|
|
44
|
+
return f"FileStack(name={self.name}, size={self.size}, is_hashable={self.is_hashable}, in_memory={self.in_memory}, locations={self.locations})"
|
|
45
|
+
|
|
46
|
+
async def custom_model_dump(self, **kwargs) -> Dict[str, Any]:
|
|
47
|
+
dumped_data = self.model_dump()
|
|
48
|
+
del dumped_data["content"] # Exclude content from the dumped data
|
|
49
|
+
return dumped_data
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def ui_base_schema(cls, **kwargs) -> Dict[str, Any]:
|
|
53
|
+
# TODO get the model programatically
|
|
54
|
+
return {
|
|
55
|
+
"ui:field": "FileField",
|
|
56
|
+
"ui:options": {
|
|
57
|
+
"model": "simstack.models.files.FileStack",
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
@classmethod
|
|
62
|
+
def from_local_file(
|
|
63
|
+
cls,
|
|
64
|
+
path: Union[Path, str],
|
|
65
|
+
is_hashable: bool = True,
|
|
66
|
+
in_memory: bool = True,
|
|
67
|
+
secure_source: bool = False,
|
|
68
|
+
task_id: str = "",
|
|
69
|
+
):
|
|
70
|
+
"""
|
|
71
|
+
Creates a FileStack object from a local file path.
|
|
72
|
+
|
|
73
|
+
:param task_id: task_id of the task that created the file stack, used for logging and tracking
|
|
74
|
+
:type task_id: str
|
|
75
|
+
:param secure_source: specifies if the source is secure (already in a directory generated within Simstack II)
|
|
76
|
+
:type secure_source: bool
|
|
77
|
+
:param path: The path to the local file. Can be provided as a string or Path object.
|
|
78
|
+
:type path: Union[Path, str]
|
|
79
|
+
:param is_hashable: A flag indicating whether the file hash needs to be calculated.
|
|
80
|
+
:type is_hashable: bool
|
|
81
|
+
:param in_memory: Whether to store the compressed file content in memory. Defaults to True.
|
|
82
|
+
:type in_memory: bool
|
|
83
|
+
|
|
84
|
+
:return: A FileStack object containing FileInstances for the file.
|
|
85
|
+
:rtype: FileStack
|
|
86
|
+
"""
|
|
87
|
+
# TODO make a second version where a unique directory is already there
|
|
88
|
+
source_path = path if isinstance(path, Path) else Path(path)
|
|
89
|
+
|
|
90
|
+
# Check if the source exists
|
|
91
|
+
if not source_path.exists():
|
|
92
|
+
logger.error(f"Source file not found: {source_path}")
|
|
93
|
+
raise FileNotFoundError(f"Source file not found: {source_path}")
|
|
94
|
+
|
|
95
|
+
# Check if it's a file (not a directory)
|
|
96
|
+
if not source_path.is_file():
|
|
97
|
+
logger.error(f"Source file {path} is not a file")
|
|
98
|
+
raise ValueError(f"Source file is not a file: {path}")
|
|
99
|
+
|
|
100
|
+
# Check read permission using os.access
|
|
101
|
+
if not os.access(path, os.R_OK):
|
|
102
|
+
logger.error(f"No read permission for file: {path}")
|
|
103
|
+
raise ValueError(f"No read permission for file: {path}")
|
|
104
|
+
|
|
105
|
+
content = None
|
|
106
|
+
file_hash = hash_file(source_path) if is_hashable else None
|
|
107
|
+
name = source_path.name
|
|
108
|
+
size = source_path.stat().st_size if source_path.exists() else None
|
|
109
|
+
|
|
110
|
+
if in_memory:
|
|
111
|
+
try:
|
|
112
|
+
# Read the file content
|
|
113
|
+
with open(source_path, "rb") as f:
|
|
114
|
+
file_content = f.read()
|
|
115
|
+
# Compress the content using zlib
|
|
116
|
+
content = zlib.compress(file_content)
|
|
117
|
+
logger.debug(
|
|
118
|
+
f"Compressed file {source_path} from {len(file_content)} bytes to {len(content)} bytes"
|
|
119
|
+
)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
logger.warning(f"Failed to compress file {source_path}: {e}")
|
|
122
|
+
|
|
123
|
+
file_stack = cls(
|
|
124
|
+
name=name,
|
|
125
|
+
in_memory=in_memory,
|
|
126
|
+
content=content,
|
|
127
|
+
is_hashable=is_hashable,
|
|
128
|
+
size=size,
|
|
129
|
+
hash=file_hash,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
if not in_memory:
|
|
133
|
+
location = FileInstance.from_local_file(
|
|
134
|
+
path=path, file_stack_id=file_stack.id, make_copy=not secure_source
|
|
135
|
+
)
|
|
136
|
+
file_stack.locations.append(location)
|
|
137
|
+
return file_stack
|
|
138
|
+
|
|
139
|
+
def complex_hash(self) -> str:
|
|
140
|
+
if self.is_hashable:
|
|
141
|
+
if self.hash:
|
|
142
|
+
return self.hash
|
|
143
|
+
else:
|
|
144
|
+
raise ValueError("FileStack is hashable but hash is not set.")
|
|
145
|
+
#elif self.in_memory and self.content:
|
|
146
|
+
# # If the content is in memory, hash the compressed content
|
|
147
|
+
# return complex_hash_function(zlib.decompress(self.content))
|
|
148
|
+
#else:
|
|
149
|
+
# temp_dir = Path(tempfile.mkdtemp())
|
|
150
|
+
# local_file = self.get(None, local_dir=temp_dir)
|
|
151
|
+
# return complex_hash_function(local_file.read_bytes())
|
|
152
|
+
else:
|
|
153
|
+
logger.warning(
|
|
154
|
+
f"FileStack {self.id} is not hashable, returning unique hash."
|
|
155
|
+
)
|
|
156
|
+
return str(ObjectId())
|
|
157
|
+
|
|
158
|
+
def append(self, file_instance: FileInstance):
|
|
159
|
+
"""
|
|
160
|
+
Appends a FileInstance to the file stack.
|
|
161
|
+
|
|
162
|
+
:param file_instance: The FileInstance to append.
|
|
163
|
+
:type file_instance: FileInstance
|
|
164
|
+
"""
|
|
165
|
+
self.locations.append(file_instance)
|
|
166
|
+
|
|
167
|
+
def get(self, local_resource: Resource, local_dir: Path) -> Path:
|
|
168
|
+
"""
|
|
169
|
+
Copies the file stack to a local directory.
|
|
170
|
+
|
|
171
|
+
:param local_resource: the local resource to copy the file stack to. Defaults to the current resource.
|
|
172
|
+
:param local_dir: The local directory to copy the file stack to.
|
|
173
|
+
:type local_dir: Path
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
# select the best instance
|
|
177
|
+
# first search for an instance with "in_memory" set to True
|
|
178
|
+
|
|
179
|
+
if self.in_memory:
|
|
180
|
+
local_dir.mkdir(parents=True, exist_ok=True)
|
|
181
|
+
try:
|
|
182
|
+
# Decompress the content
|
|
183
|
+
decompressed_content = zlib.decompress(self.content)
|
|
184
|
+
# Write the decompressed content to the local directory
|
|
185
|
+
with open(local_dir / self.name, "wb") as f:
|
|
186
|
+
f.write(decompressed_content)
|
|
187
|
+
return local_dir / self.name
|
|
188
|
+
except Exception as e:
|
|
189
|
+
logger.error(f"Failed to decompress and write file {self.name}: {e}")
|
|
190
|
+
raise ValueError(
|
|
191
|
+
f"Failed to decompress and write file {self.name}: {e}"
|
|
192
|
+
)
|
|
193
|
+
# If in-memory instance not found or decompression failed, try finding instance on same resource
|
|
194
|
+
if same_resource_instance := next(
|
|
195
|
+
(f for f in self.locations if f.resource == local_resource), None
|
|
196
|
+
):
|
|
197
|
+
# Copy the file from the source path to the destination path
|
|
198
|
+
return Path(same_resource_instance.path)
|
|
199
|
+
else:
|
|
200
|
+
local_dir.mkdir(parents=True, exist_ok=True)
|
|
201
|
+
logger.error("No suitable file instance found for copying.")
|
|
202
|
+
from simstack.methods.get_file import get_file
|
|
203
|
+
return get_file(self,local_resource, local_dir / self.name)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def str(self):
|
|
207
|
+
return f"FileStack(name={self.name}, size={self.size}, is_hashable={self.is_hashable}, in_memory={self.in_memory}, locations={self.locations})"
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
async def main():
|
|
211
|
+
from simstack.core.context import context
|
|
212
|
+
|
|
213
|
+
context.initialize()
|
|
214
|
+
# write a file test.txt
|
|
215
|
+
with open("test.txt", "w") as f:
|
|
216
|
+
f.write("Hello World")
|
|
217
|
+
file_stack = FileStack.from_local_file("test.txt", is_hashable=True, in_memory=True)
|
|
218
|
+
print(file_stack)
|
|
219
|
+
await context.db.save(file_stack)
|
|
220
|
+
local_dir = Path(context.config.workdir) / "samira" / str(file_stack.id)
|
|
221
|
+
retrieved = file_stack.get(context.config.resource, local_dir=local_dir)
|
|
222
|
+
print("Retrieved file path:", retrieved)
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
import asyncio
|
|
227
|
+
|
|
228
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
229
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from odmantic import Model, Field
|
|
6
|
+
|
|
7
|
+
from simstack.models import Parameters
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LogLevel(str, Enum):
|
|
11
|
+
DEBUG = "DEBUG"
|
|
12
|
+
INFO = "INFO"
|
|
13
|
+
WARNING = "WARNING"
|
|
14
|
+
ERROR = "ERROR"
|
|
15
|
+
CRITICAL = "CRITICAL"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LogEntry(Model):
|
|
19
|
+
timestamp: datetime = Field(default_factory=datetime.utcnow)
|
|
20
|
+
level: LogLevel # Use enum for validation
|
|
21
|
+
logger_name: str
|
|
22
|
+
message: str
|
|
23
|
+
module: str
|
|
24
|
+
function: str
|
|
25
|
+
line: int
|
|
26
|
+
task_id: Optional[str] = None
|
|
27
|
+
resource: Optional[str] = None
|
|
28
|
+
thread_name: Optional[str] = None
|
|
29
|
+
process_name: Optional[str] = None
|
|
30
|
+
exception_type: Optional[str] = None
|
|
31
|
+
exception_message: Optional[str] = None
|
|
32
|
+
exception_traceback: Optional[str] = None
|
|
33
|
+
# extra_data: Dict[str, Any] = Field(default_factory=dict)
|
|
34
|
+
parameters: Optional[Parameters] = None # If Parameters is related to LogEntry
|
|
35
|
+
|
|
36
|
+
# Replace Config class with model_config
|
|
37
|
+
model_config = {"collection": "logs"}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from typing import Optional, List, TypeVar
|
|
3
|
+
from odmantic import Model, Field
|
|
4
|
+
from simstack.models.parameters import Parameters
|
|
5
|
+
from simstack.models.pickle_models import ClassPickle, FunctionPickle
|
|
6
|
+
import logging
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("Models")
|
|
9
|
+
|
|
10
|
+
T = TypeVar("T")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ModelMapping(Model):
|
|
14
|
+
"""
|
|
15
|
+
name: shorthand - must be unique
|
|
16
|
+
mapping: full name - path relative to project root in module.module.class/function format
|
|
17
|
+
"""
|
|
18
|
+
name: str = Field(unique=True)
|
|
19
|
+
mapping: str = Field(unique=True)
|
|
20
|
+
collection_name: str
|
|
21
|
+
json_schema: Optional[str] = None
|
|
22
|
+
ui_schema: Optional[str] = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class NodeModel(Model):
|
|
27
|
+
name: str = Field(unique=True)
|
|
28
|
+
function_mapping: str = Field(unique=True)
|
|
29
|
+
input_mappings: List[str]
|
|
30
|
+
description: Optional[str] = ""
|
|
31
|
+
favorite: bool = False
|
|
32
|
+
default_parameters: Parameters
|
|
33
|
+
pickle_function: Optional[
|
|
34
|
+
FunctionPickle
|
|
35
|
+
] = None # Reference to FunctionPickle if available
|
|
36
|
+
|
|
37
|
+
model_config = {
|
|
38
|
+
"collection": "node_model",
|
|
39
|
+
"json_encoders": {bytes: lambda b: base64.b64encode(b).decode("ascii")},
|
|
40
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
|
|
4
|
+
from odmantic import Model, Field, ObjectId, Reference
|
|
5
|
+
|
|
6
|
+
from simstack.core.definitions import TaskStatus
|
|
7
|
+
from simstack.models.file_list import FileList
|
|
8
|
+
from simstack.models.parameters import Parameters
|
|
9
|
+
from simstack.core.engine import current_engine_context
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class NodeRegistry(Model):
|
|
13
|
+
"""
|
|
14
|
+
Represents a registry for nodes with associated metadata, configurations, and status
|
|
15
|
+
information. It allows tracking the state and attributes of a workflow node, including its
|
|
16
|
+
execution parameters, process status, and relationship to other nodes.
|
|
17
|
+
|
|
18
|
+
This class is designed for managing workflow instances and their lifecycle, with
|
|
19
|
+
capabilities to monitor execution states, inputs, outputs, and associated timestamps.
|
|
20
|
+
|
|
21
|
+
:ivar name: The name of the node.
|
|
22
|
+
:type name: str
|
|
23
|
+
:ivar custom_name: An optional custom name for the node.
|
|
24
|
+
:type custom_name: Optional[str]
|
|
25
|
+
:ivar status: TheTaskStatus of the node in string format.
|
|
26
|
+
:type status: TaskStatus
|
|
27
|
+
:ivar category: An optional category classification for the node.
|
|
28
|
+
:type category: Optional[str]
|
|
29
|
+
:ivar description: An optional description providing details about the node.
|
|
30
|
+
:type description: Optional[str]
|
|
31
|
+
:ivar input_tables: The names of the classes associated with this node's inputs.
|
|
32
|
+
:type input_tables: List[str]
|
|
33
|
+
:ivar input_ids: The unique identifier for the specific workflow instance providing the
|
|
34
|
+
inputs.
|
|
35
|
+
:type input_ids: List[ObjectId]
|
|
36
|
+
:ivar result_tables: list of names of the result class of the ode's outputs.
|
|
37
|
+
:type result_tables: List[str]
|
|
38
|
+
:ivar result_ids: List of ids for the results instance.
|
|
39
|
+
:type result_ids: ListObjectId]
|
|
40
|
+
:ivar parent_ids: A list of identifiers representing parent nodes associated with this node.
|
|
41
|
+
:type parent_ids: List[ObjectId]
|
|
42
|
+
:ivar created_at: The timestamp when the node was created.
|
|
43
|
+
:type created_at: datetime
|
|
44
|
+
:ivar started_at: An optional timestamp indicating when the execution of the node started.
|
|
45
|
+
:type started_at: Optional[datetime]
|
|
46
|
+
:ivar completed_at: An optional timestamp indicating when the execution of the node was
|
|
47
|
+
completed.
|
|
48
|
+
:type completed_at: Optional[datetime]
|
|
49
|
+
:ivar function_hash: A hash value representing the unique function executed by this node.
|
|
50
|
+
:type function_hash: str
|
|
51
|
+
:ivar arg_hash: A hash value representing the unique arguments passed to the function of
|
|
52
|
+
this node.
|
|
53
|
+
:type arg_hash: str
|
|
54
|
+
:ivar func_mapping: A mapping identifier associated with the function executed by this
|
|
55
|
+
node.
|
|
56
|
+
:type func_mapping: str
|
|
57
|
+
:ivar is_async: A boolean value indicating whether the node execution is asynchronous.
|
|
58
|
+
:type is_async: bool
|
|
59
|
+
:ivar parameters: Parameters associated with the node execution.
|
|
60
|
+
:type parameters: Parameters
|
|
61
|
+
:ivar call_path: An optional path indicating where the function is called from by concatenating the name of all nodes in the call stack.
|
|
62
|
+
:type call_path: Optional[str]
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
name: str
|
|
66
|
+
status: TaskStatus
|
|
67
|
+
custom_name: Optional[str] = None
|
|
68
|
+
project: Optional[str] = "default"
|
|
69
|
+
category: Optional[str] = None
|
|
70
|
+
description: Optional[str] = None
|
|
71
|
+
call_path: Optional[str] = None
|
|
72
|
+
error: Optional[str] = None
|
|
73
|
+
message: Optional[str] = None
|
|
74
|
+
|
|
75
|
+
input_names: List[str] = Field(default_factory=list)
|
|
76
|
+
input_tables: List[str] = Field(
|
|
77
|
+
default_factory=list
|
|
78
|
+
) # The model mappings the input classes
|
|
79
|
+
input_ids: List[ObjectId] = Field(
|
|
80
|
+
default_factory=list
|
|
81
|
+
) # The ID of the specific workflow instance
|
|
82
|
+
result_tables: List[str] = Field(
|
|
83
|
+
default_factory=list
|
|
84
|
+
) # The model mappings of the result classes
|
|
85
|
+
result_ids: List[ObjectId] = Field(default_factory=list)
|
|
86
|
+
result_names: List[str] = Field(default_factory=list)
|
|
87
|
+
|
|
88
|
+
info_files: FileList = Field(default=FileList())
|
|
89
|
+
|
|
90
|
+
parent_ids: List[ObjectId] = Field(default_factory=list)
|
|
91
|
+
artifact_ids: List[ObjectId] = Field(default_factory=list)
|
|
92
|
+
created_at: datetime = Field(default_factory=datetime.now)
|
|
93
|
+
started_at: Optional[datetime] = Field(default=None)
|
|
94
|
+
completed_at: Optional[datetime] = Field(default=None)
|
|
95
|
+
job_id: Optional[str] = None
|
|
96
|
+
function_hash: str
|
|
97
|
+
arg_hash: str
|
|
98
|
+
func_mapping: str
|
|
99
|
+
is_async: bool = False
|
|
100
|
+
parameters: Parameters = Reference()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
async def find_child_nodes(task_id: str) -> List[NodeRegistry]:
|
|
104
|
+
engine = current_engine_context.get()
|
|
105
|
+
return await engine.find(NodeRegistry, {"parent_ids": {"$in": [task_id]}})
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import io
|
|
3
|
+
import json
|
|
4
|
+
from pprint import pprint
|
|
5
|
+
from typing import Dict, Any
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from odmantic import Model
|
|
10
|
+
|
|
11
|
+
from simstack.core.context import context
|
|
12
|
+
from simstack.models import simstack_model
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _format_datetime_columns(df):
|
|
16
|
+
"""
|
|
17
|
+
Format all datetime columns to match the expected format: YYYY-MM-DDThh:mm:ss
|
|
18
|
+
Returns a copy of the dataframe with formatted datetime columns.
|
|
19
|
+
"""
|
|
20
|
+
df_copy = df.copy()
|
|
21
|
+
# Format datetime columns to match expected format without microseconds and Z
|
|
22
|
+
for col in df_copy.select_dtypes(include=["datetime64"]):
|
|
23
|
+
df_copy[col] = df_copy[col].dt.strftime("%Y-%m-%dT%H:%M:%S")
|
|
24
|
+
return df_copy
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@simstack_model
|
|
28
|
+
class PandasModel(Model):
|
|
29
|
+
content_: bytes = b""
|
|
30
|
+
|
|
31
|
+
@classmethod
|
|
32
|
+
def from_data_frame(cls, df):
|
|
33
|
+
new_instance = cls()
|
|
34
|
+
new_instance.table = df
|
|
35
|
+
return new_instance
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def table(self):
|
|
39
|
+
if not self.content_:
|
|
40
|
+
return pd.DataFrame()
|
|
41
|
+
|
|
42
|
+
# Create a BytesIO object from the binary content
|
|
43
|
+
buffer = io.BytesIO(self.content_)
|
|
44
|
+
|
|
45
|
+
# Use pandas read_pickle to decompress and load the DataFrame
|
|
46
|
+
return pd.read_pickle(buffer)
|
|
47
|
+
|
|
48
|
+
@table.setter
|
|
49
|
+
def table(self, df):
|
|
50
|
+
if not isinstance(df, pd.DataFrame):
|
|
51
|
+
raise TypeError("Expected a pandas DataFrame")
|
|
52
|
+
|
|
53
|
+
# Create a BytesIO object to store the binary content
|
|
54
|
+
buffer = io.BytesIO()
|
|
55
|
+
|
|
56
|
+
# Serialize the DataFrame to the buffer
|
|
57
|
+
df.to_pickle(buffer)
|
|
58
|
+
|
|
59
|
+
# Get the binary content from the buffer
|
|
60
|
+
self.content_ = buffer.getvalue()
|
|
61
|
+
|
|
62
|
+
def to_react_json(self, orient="records"):
|
|
63
|
+
"""
|
|
64
|
+
Convert the DataFrame to a JSON string suitable for React visualization libraries.
|
|
65
|
+
|
|
66
|
+
Parameters:
|
|
67
|
+
- orient: Determines the JSON string layout:
|
|
68
|
+
'records' - list like [{column -> value}, ... , {column -> value}] (default)
|
|
69
|
+
'columns' - {column -> [values, ...]}
|
|
70
|
+
'index' - {column -> value}}
|
|
71
|
+
'split' - {index -> [index], columns -> [columns], data -> [values]}
|
|
72
|
+
'table' - {'schema': {schema}, 'data': {data}}
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
- String: JSON formatted string ready for React
|
|
76
|
+
"""
|
|
77
|
+
df = self.table
|
|
78
|
+
if df.empty:
|
|
79
|
+
return json.dumps([])
|
|
80
|
+
|
|
81
|
+
# Format datetime columns
|
|
82
|
+
df = _format_datetime_columns(df)
|
|
83
|
+
|
|
84
|
+
# Create a custom serializer to handle NumPy types properly
|
|
85
|
+
class NumpyEncoder(json.JSONEncoder):
|
|
86
|
+
def default(self, obj):
|
|
87
|
+
if isinstance(obj, np.integer):
|
|
88
|
+
return int(obj) # Keep integers as integers
|
|
89
|
+
elif isinstance(obj, np.floating):
|
|
90
|
+
return float(obj) # Convert numpy float to Python float
|
|
91
|
+
elif isinstance(obj, np.ndarray):
|
|
92
|
+
return obj.tolist() # Convert arrays to lists
|
|
93
|
+
elif pd.isna(obj):
|
|
94
|
+
return None # Convert NaN/NaT to None
|
|
95
|
+
return super().default(obj)
|
|
96
|
+
|
|
97
|
+
# Convert DataFrame to dictionary while preserving data types
|
|
98
|
+
if orient == "records":
|
|
99
|
+
data = df.to_dict(orient="records")
|
|
100
|
+
else:
|
|
101
|
+
data = df.to_dict(orient=orient)
|
|
102
|
+
|
|
103
|
+
# Use custom JSON encoder to handle NumPy types properly
|
|
104
|
+
return json.dumps(data, cls=NumpyEncoder)
|
|
105
|
+
|
|
106
|
+
def to_react_data(self, orient="records"):
|
|
107
|
+
"""
|
|
108
|
+
Convert the DataFrame to a Python object suitable for conversion to JSON.
|
|
109
|
+
This can be used in API responses.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
- List/Dict: Python object ready for json.dumps()
|
|
113
|
+
"""
|
|
114
|
+
df = self.table
|
|
115
|
+
if df.empty:
|
|
116
|
+
return []
|
|
117
|
+
|
|
118
|
+
# Format datetime columns using the same helper method
|
|
119
|
+
df = _format_datetime_columns(df)
|
|
120
|
+
|
|
121
|
+
# Convert DataFrame to dictionary while preserving data types
|
|
122
|
+
if orient == "records":
|
|
123
|
+
data = df.to_dict(orient="records")
|
|
124
|
+
else:
|
|
125
|
+
data = df.to_dict(orient=orient)
|
|
126
|
+
|
|
127
|
+
# Replace NaN values with None
|
|
128
|
+
if isinstance(data, list):
|
|
129
|
+
for item in data:
|
|
130
|
+
for key, value in item.items():
|
|
131
|
+
if pd.isna(value):
|
|
132
|
+
item[key] = None
|
|
133
|
+
elif isinstance(data, dict):
|
|
134
|
+
for key, values in data.items():
|
|
135
|
+
if isinstance(values, dict):
|
|
136
|
+
for sub_key, value in values.items():
|
|
137
|
+
if pd.isna(value):
|
|
138
|
+
values[sub_key] = None
|
|
139
|
+
elif isinstance(values, list):
|
|
140
|
+
data[key] = [None if pd.isna(v) else v for v in values]
|
|
141
|
+
|
|
142
|
+
return data
|
|
143
|
+
|
|
144
|
+
async def custom_model_dump(self, **kwargs) -> Dict[str, Any]:
|
|
145
|
+
dumped_data = self.to_react_data("dict")
|
|
146
|
+
# del dumped_data["content"] # Exclude content from the dumped data
|
|
147
|
+
return dumped_data
|
|
148
|
+
|
|
149
|
+
def __repr__(self):
|
|
150
|
+
if not self.content_:
|
|
151
|
+
return "PandasModel(empty table)"
|
|
152
|
+
|
|
153
|
+
df = self.table
|
|
154
|
+
rows, cols = df.shape
|
|
155
|
+
return f"PandasModel({rows} rows × {cols} columns)"
|
|
156
|
+
|
|
157
|
+
def __str__(self):
|
|
158
|
+
if not self.content_:
|
|
159
|
+
return "Empty pandas table"
|
|
160
|
+
|
|
161
|
+
df = self.table
|
|
162
|
+
if len(df) > 5:
|
|
163
|
+
return f"PandasModel with shape {df.shape}:\n{df.head(5).to_string()}\n..."
|
|
164
|
+
return f"PandasModel with shape {df.shape}:\n{df.to_string()}"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
async def main():
|
|
168
|
+
context.initialize()
|
|
169
|
+
# Create the data structure
|
|
170
|
+
data = []
|
|
171
|
+
|
|
172
|
+
# Iteration names
|
|
173
|
+
iterations = ["iter1", "iter2", "iter3"]
|
|
174
|
+
|
|
175
|
+
# For each iteration
|
|
176
|
+
for index, iteration in enumerate(iterations):
|
|
177
|
+
# Generate 4 x values (for example, increasing by 0.5)
|
|
178
|
+
x_values = np.arange(1, 3, 0.5) # Creates [1.0, 1.5, 2.0, 2.5]
|
|
179
|
+
|
|
180
|
+
# Generate 2 sets of 4 y values for each iteration
|
|
181
|
+
y_values_set1 = np.sin((index + 1) * x_values) # 4 random values around mean=8
|
|
182
|
+
y_values_set2 = np.sin((index + 1) * x_values)
|
|
183
|
+
# Add the data for this iteration
|
|
184
|
+
for i, x in enumerate(x_values):
|
|
185
|
+
data.append(
|
|
186
|
+
{
|
|
187
|
+
"iteration": iteration,
|
|
188
|
+
"x": x,
|
|
189
|
+
"y_set1": y_values_set1[i],
|
|
190
|
+
"y_set2": y_values_set2[i],
|
|
191
|
+
}
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Create the DataFrame
|
|
195
|
+
df = pd.DataFrame(data)
|
|
196
|
+
print(df.style.format(precision=2))
|
|
197
|
+
|
|
198
|
+
model = PandasModel.from_data_frame(df)
|
|
199
|
+
|
|
200
|
+
pprint(await model.custom_model_dump())
|
|
201
|
+
saved_model = await context.db.save(model)
|
|
202
|
+
|
|
203
|
+
retrieved_model = await context.db.find_one(
|
|
204
|
+
PandasModel, PandasModel.id == saved_model.id
|
|
205
|
+
)
|
|
206
|
+
print("Retrieved Model", retrieved_model)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
if __name__ == "__main__":
|
|
210
|
+
asyncio.run(main())
|