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.
Files changed (91) hide show
  1. simstack/__init__.py +0 -0
  2. simstack/_version.py +34 -0
  3. simstack/core/__init__.py +0 -0
  4. simstack/core/artifacts.py +447 -0
  5. simstack/core/asnyc_helper.py +14 -0
  6. simstack/core/config_file.py +60 -0
  7. simstack/core/context.py +167 -0
  8. simstack/core/data.py +139 -0
  9. simstack/core/definitions.py +21 -0
  10. simstack/core/engine.py +116 -0
  11. simstack/core/find_simstack_modules.py +43 -0
  12. simstack/core/hash.py +177 -0
  13. simstack/core/model_table.py +199 -0
  14. simstack/core/node.py +911 -0
  15. simstack/core/node_runner.py +254 -0
  16. simstack/core/node_table.py +380 -0
  17. simstack/core/recompute_artifacts.py +120 -0
  18. simstack/core/reset_db.py +12 -0
  19. simstack/core/resources.py +101 -0
  20. simstack/core/run_node.py +69 -0
  21. simstack/core/runner.py +435 -0
  22. simstack/core/simstack_result.py +41 -0
  23. simstack/core/submit_node.py +143 -0
  24. simstack/core/task_id.py +22 -0
  25. simstack/data/__init__.py +0 -0
  26. simstack/methods/__init__.py +0 -0
  27. simstack/methods/get_file.py +10 -0
  28. simstack/methods/multiple_executor.py +307 -0
  29. simstack/models/__init__.py +37 -0
  30. simstack/models/array_list.py +48 -0
  31. simstack/models/array_storage.py +54 -0
  32. simstack/models/artifact_models.py +127 -0
  33. simstack/models/base_types.py +208 -0
  34. simstack/models/charts_artifact.py +411 -0
  35. simstack/models/dataset.py +432 -0
  36. simstack/models/dataset_metadata.py +303 -0
  37. simstack/models/file_instance.py +77 -0
  38. simstack/models/file_list.py +321 -0
  39. simstack/models/files.py +229 -0
  40. simstack/models/log_entry_model.py +37 -0
  41. simstack/models/models.py +40 -0
  42. simstack/models/node_registry.py +105 -0
  43. simstack/models/pandas_model.py +210 -0
  44. simstack/models/parameters.py +388 -0
  45. simstack/models/pickle_models.py +311 -0
  46. simstack/models/runner_model.py +39 -0
  47. simstack/models/simple_table.py +32 -0
  48. simstack/models/simstack_model.py +97 -0
  49. simstack/models/slurm_info.py +17 -0
  50. simstack/models/table_artifact.py +87 -0
  51. simstack/util/__init__.py +0 -0
  52. simstack/util/async_zip_utils.py +18 -0
  53. simstack/util/cleaned_json_schema.py +20 -0
  54. simstack/util/config_reader.py +273 -0
  55. simstack/util/custom_model_dump.py +75 -0
  56. simstack/util/db.py +457 -0
  57. simstack/util/db_logger.py +117 -0
  58. simstack/util/default_from_dict.py +57 -0
  59. simstack/util/directory_iterator.py +216 -0
  60. simstack/util/file_hashing.py +68 -0
  61. simstack/util/files.py +396 -0
  62. simstack/util/find_python_files.py +33 -0
  63. simstack/util/generate_ui_schema.py +80 -0
  64. simstack/util/geometry_utils.py +118 -0
  65. simstack/util/get_module_path.py +26 -0
  66. simstack/util/git_pull.py +65 -0
  67. simstack/util/git_repository_status.py +93 -0
  68. simstack/util/import_module.py +65 -0
  69. simstack/util/importer.py +163 -0
  70. simstack/util/make_graph.py +107 -0
  71. simstack/util/make_table.py +827 -0
  72. simstack/util/minimal_route_finder.py +72 -0
  73. simstack/util/mock_db.py +144 -0
  74. simstack/util/module_path_checker.py +38 -0
  75. simstack/util/path_manager.py +234 -0
  76. simstack/util/project_root_finder.py +36 -0
  77. simstack/util/queue_watcher.py +262 -0
  78. simstack/util/route_finder.py +74 -0
  79. simstack/util/runner_util.py +162 -0
  80. simstack/util/safe_code_executor.py +194 -0
  81. simstack/util/sample_config.py +98 -0
  82. simstack/util/sensitive_content_filter.py +48 -0
  83. simstack/util/setup_logging.py +49 -0
  84. simstack/util/submit_to_watchdog.py +106 -0
  85. simstack/util/ui_tools.py +213 -0
  86. simstack-0.2.0.dist-info/METADATA +361 -0
  87. simstack-0.2.0.dist-info/RECORD +91 -0
  88. simstack-0.2.0.dist-info/WHEEL +5 -0
  89. simstack-0.2.0.dist-info/entry_points.txt +10 -0
  90. simstack-0.2.0.dist-info/licenses/LICENSE +21 -0
  91. simstack-0.2.0.dist-info/top_level.txt +1 -0
@@ -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())