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
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from typing import Dict, Any, Union, List
|
|
3
|
+
|
|
4
|
+
from odmantic import Model, EmbeddedModel, Field
|
|
5
|
+
|
|
6
|
+
from simstack.core.asnyc_helper import async_helper
|
|
7
|
+
from simstack.core.context import context
|
|
8
|
+
from simstack.core.engine import current_engine_context
|
|
9
|
+
from simstack.models import simstack_model
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _get_json_schema(data: Dict) -> dict:
|
|
13
|
+
"""
|
|
14
|
+
Inspects the current data dict and returns a JSON schema for that dict.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
dict: JSON schema describing the structure and types of the current data dict
|
|
18
|
+
"""
|
|
19
|
+
if not data:
|
|
20
|
+
return {"type": "object", "properties": {}, "additionalProperties": False}
|
|
21
|
+
|
|
22
|
+
properties = {}
|
|
23
|
+
|
|
24
|
+
for key, value in data.items():
|
|
25
|
+
if isinstance(value, str):
|
|
26
|
+
properties[key] = {"type": "string"}
|
|
27
|
+
elif isinstance(value, bool):
|
|
28
|
+
# Check bool before int/float since bool is a subclass of int in Python
|
|
29
|
+
properties[key] = {"type": "boolean"}
|
|
30
|
+
elif isinstance(value, int):
|
|
31
|
+
properties[key] = {"type": "integer"}
|
|
32
|
+
elif isinstance(value, float):
|
|
33
|
+
properties[key] = {"type": "number"}
|
|
34
|
+
elif isinstance(value, datetime):
|
|
35
|
+
properties[key] = {"type": "string", "format": "date-time"}
|
|
36
|
+
else:
|
|
37
|
+
# Fallback for any unexpected types
|
|
38
|
+
properties[key] = {"type": "string"}
|
|
39
|
+
|
|
40
|
+
schema = {"type": "object", "properties": properties, "additionalProperties": False}
|
|
41
|
+
|
|
42
|
+
return schema
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class DataSetMetadataTemplate(Model):
|
|
46
|
+
dataset_type: str
|
|
47
|
+
model_json: dict[str, Any]
|
|
48
|
+
structure: Dict[str, List[str]] = Field(default_factory=dict)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@simstack_model
|
|
52
|
+
class DataSetMetadata(EmbeddedModel):
|
|
53
|
+
dataset_type: str = Field(unique=True)
|
|
54
|
+
data: Dict[str, Union[str, int, float, bool, datetime]] = Field(
|
|
55
|
+
default_factory=dict
|
|
56
|
+
)
|
|
57
|
+
is_validated: bool = False
|
|
58
|
+
structure: Dict[str, List[str]] = Field(default_factory=dict)
|
|
59
|
+
|
|
60
|
+
def get_json_schema(self):
|
|
61
|
+
return _get_json_schema(self.data)
|
|
62
|
+
|
|
63
|
+
async def validate_dict(self, new_structure: Dict[str, List[str]]) -> bool:
|
|
64
|
+
engine = current_engine_context.get()
|
|
65
|
+
reference_metadata = await engine.find_one(
|
|
66
|
+
DataSetMetadataTemplate,
|
|
67
|
+
DataSetMetadataTemplate.dataset_type == self.dataset_type,
|
|
68
|
+
)
|
|
69
|
+
if reference_metadata is None:
|
|
70
|
+
metadata_template = DataSetMetadataTemplate(
|
|
71
|
+
dataset_type=self.dataset_type,
|
|
72
|
+
model_json=_get_json_schema(self.data),
|
|
73
|
+
structure=new_structure,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
await context.db.save(metadata_template)
|
|
77
|
+
return True # first model of this type
|
|
78
|
+
|
|
79
|
+
new_data_json = _get_json_schema(self.data)
|
|
80
|
+
if reference_metadata.model_json != new_data_json:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"Data schema has changed in the database reference: {reference_metadata.model_json} current: {new_data_json}"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Check if lists in existing sections match
|
|
86
|
+
save_template = False
|
|
87
|
+
for section, content in new_structure.items():
|
|
88
|
+
if content: # if there are no elements in a section this should be None
|
|
89
|
+
if section in reference_metadata.structure:
|
|
90
|
+
if content != reference_metadata.structure[section]:
|
|
91
|
+
raise ValueError(
|
|
92
|
+
f"Section {section} has different content in existing structure"
|
|
93
|
+
)
|
|
94
|
+
else:
|
|
95
|
+
save_template = True
|
|
96
|
+
self.structure[section] = content
|
|
97
|
+
else:
|
|
98
|
+
# if there is no content in a section, we can just copy it from the reference metadata
|
|
99
|
+
new_structure[section] = reference_metadata.structure[section]
|
|
100
|
+
|
|
101
|
+
if save_template:
|
|
102
|
+
reference_metadata.structure = new_structure
|
|
103
|
+
await engine.save(reference_metadata)
|
|
104
|
+
self.structure = new_structure
|
|
105
|
+
return True
|
|
106
|
+
|
|
107
|
+
@async_helper
|
|
108
|
+
async def freeze(self, new_structure: Dict[str, Dict[str, Any]]) -> bool:
|
|
109
|
+
engine = current_engine_context.get()
|
|
110
|
+
reference_metadata = await engine.find_one(
|
|
111
|
+
DataSetMetadataTemplate,
|
|
112
|
+
DataSetMetadataTemplate.dataset_type == self.dataset_type,
|
|
113
|
+
)
|
|
114
|
+
if not reference_metadata:
|
|
115
|
+
raise ValueError("Metadata does not exist")
|
|
116
|
+
if self.structure != reference_metadata.structure:
|
|
117
|
+
raise ValueError("Metadata structure has changed in the database")
|
|
118
|
+
if self.structure == {}:
|
|
119
|
+
self.structure = new_structure
|
|
120
|
+
reference_metadata.structure = new_structure
|
|
121
|
+
await engine.save(reference_metadata)
|
|
122
|
+
# some structure exists already
|
|
123
|
+
return new_structure == self.structure
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def initialized(self) -> bool:
|
|
127
|
+
"""Check if the model has been fully constructed."""
|
|
128
|
+
# A simple heuristic: if we have an ID or if type is set, we're initialized
|
|
129
|
+
return hasattr(self, "dataset_type") and self.dataset_type is not None
|
|
130
|
+
|
|
131
|
+
# Dict-like behavior methods
|
|
132
|
+
def __getitem__(self, key: str):
|
|
133
|
+
"""Get item from data dict."""
|
|
134
|
+
return self.data[key]
|
|
135
|
+
|
|
136
|
+
def __setitem__(self, key: str, value: Union[str, int, float, bool, datetime]):
|
|
137
|
+
"""Set item in data dict with validation."""
|
|
138
|
+
# Validate value type
|
|
139
|
+
if not isinstance(value, (str, int, float, bool, datetime)):
|
|
140
|
+
raise TypeError(
|
|
141
|
+
f"Value must be str, int, float, bool, or datetime, got {type(value).__name__}"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# Only check for structural changes after initialization
|
|
145
|
+
if self.initialized and key not in self.data:
|
|
146
|
+
raise KeyError(
|
|
147
|
+
f"Cannot add new key '{key}' after initialization. Existing keys: {list(self.data.keys())}"
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
# Only check for type changes after initialization
|
|
151
|
+
if self.initialized and key in self.data:
|
|
152
|
+
existing_value = self.data[key]
|
|
153
|
+
if type(existing_value) != type(value):
|
|
154
|
+
raise TypeError(
|
|
155
|
+
f"Cannot change type of key '{key}' from {type(existing_value).__name__} "
|
|
156
|
+
f"to {type(value).__name__}"
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
self.data[key] = value
|
|
160
|
+
|
|
161
|
+
def __delitem__(self, key: str):
|
|
162
|
+
"""Delete item from data dict."""
|
|
163
|
+
if self.initialized:
|
|
164
|
+
raise KeyError(f"Cannot delete key '{key}' after initialization")
|
|
165
|
+
del self.data[key]
|
|
166
|
+
|
|
167
|
+
def __contains__(self, key: str) -> bool:
|
|
168
|
+
"""Check if key exists in data dict."""
|
|
169
|
+
return key in self.data
|
|
170
|
+
|
|
171
|
+
def __iter__(self):
|
|
172
|
+
"""Iterate over keys in data dict."""
|
|
173
|
+
return iter(self.data)
|
|
174
|
+
|
|
175
|
+
def __len__(self) -> int:
|
|
176
|
+
"""Get number of items in data dict."""
|
|
177
|
+
return len(self.data)
|
|
178
|
+
|
|
179
|
+
def keys(self):
|
|
180
|
+
"""Get keys from data dict."""
|
|
181
|
+
return self.data.keys()
|
|
182
|
+
|
|
183
|
+
def values(self):
|
|
184
|
+
"""Get values from data dict."""
|
|
185
|
+
return self.data.values()
|
|
186
|
+
|
|
187
|
+
def items(self):
|
|
188
|
+
"""Get items from data dict."""
|
|
189
|
+
return self.data.items()
|
|
190
|
+
|
|
191
|
+
def get(self, key: str, default=None):
|
|
192
|
+
"""Get item from data dict with default."""
|
|
193
|
+
return self.data.get(key, default)
|
|
194
|
+
|
|
195
|
+
def pop(self, key: str, *args):
|
|
196
|
+
"""Pop item from data dict."""
|
|
197
|
+
if self.initialized:
|
|
198
|
+
raise KeyError(f"Cannot pop key '{key}' after initialization")
|
|
199
|
+
return self.data.pop(key, *args)
|
|
200
|
+
|
|
201
|
+
def popitem(self):
|
|
202
|
+
"""Pop item from data dict."""
|
|
203
|
+
if self.initialized:
|
|
204
|
+
raise KeyError("Cannot pop items after initialization")
|
|
205
|
+
return self.data.popitem()
|
|
206
|
+
|
|
207
|
+
def clear(self):
|
|
208
|
+
"""Clear data dict."""
|
|
209
|
+
if self.initialized:
|
|
210
|
+
raise RuntimeError("Cannot clear data after initialization")
|
|
211
|
+
self.data.clear()
|
|
212
|
+
|
|
213
|
+
def update(self, *args, **kwargs):
|
|
214
|
+
"""Update data dict with validation."""
|
|
215
|
+
# Handle different update signatures
|
|
216
|
+
if args:
|
|
217
|
+
other = args[0]
|
|
218
|
+
if hasattr(other, "items"):
|
|
219
|
+
items_to_update = other.items()
|
|
220
|
+
else:
|
|
221
|
+
items_to_update = other
|
|
222
|
+
else:
|
|
223
|
+
items_to_update = []
|
|
224
|
+
|
|
225
|
+
# Combine with kwargs
|
|
226
|
+
all_items = list(items_to_update) + list(kwargs.items())
|
|
227
|
+
|
|
228
|
+
# Validate all items before updating
|
|
229
|
+
for key, value in all_items:
|
|
230
|
+
# Type validation (always required)
|
|
231
|
+
if not isinstance(value, (str, int, float, bool, datetime)):
|
|
232
|
+
raise TypeError(
|
|
233
|
+
f"Value for key '{key}' must be str, int, float, bool, or datetime, got {type(value).__name__}"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Structural validation (only after initialization)
|
|
237
|
+
if self.initialized:
|
|
238
|
+
if key not in self.data:
|
|
239
|
+
raise KeyError(f"Cannot add new key '{key}' after initialization")
|
|
240
|
+
|
|
241
|
+
existing_value = self.data[key]
|
|
242
|
+
if type(existing_value) != type(value):
|
|
243
|
+
raise TypeError(
|
|
244
|
+
f"Cannot change type of key '{key}' from {type(existing_value).__name__} "
|
|
245
|
+
f"to {type(value).__name__}"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
# If all validations pass, update the data
|
|
249
|
+
for key, value in all_items:
|
|
250
|
+
self.data[key] = value
|
|
251
|
+
|
|
252
|
+
def setdefault(self, key: str, default=None):
|
|
253
|
+
"""Set default value for key if not exists."""
|
|
254
|
+
if key not in self.data:
|
|
255
|
+
if self.initialized:
|
|
256
|
+
raise KeyError(f"Cannot add new key '{key}' after initialization")
|
|
257
|
+
|
|
258
|
+
if default is not None and not isinstance(
|
|
259
|
+
default, (str, int, float, bool, datetime)
|
|
260
|
+
):
|
|
261
|
+
raise TypeError(
|
|
262
|
+
f"Default value must be str, int, float, bool, or datetime, got {type(default).__name__}"
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
self.data[key] = default
|
|
266
|
+
|
|
267
|
+
return self.data[key]
|
|
268
|
+
|
|
269
|
+
# Additional utility methods (no initialization checks needed)
|
|
270
|
+
def copy_data(self) -> Dict[str, Union[str, int, float, bool, datetime]]:
|
|
271
|
+
"""Return a copy of the data dict."""
|
|
272
|
+
return self.data.copy()
|
|
273
|
+
|
|
274
|
+
def is_type_compatible(self, key: str, value) -> bool:
|
|
275
|
+
"""Check if a value is type-compatible with existing key."""
|
|
276
|
+
if key not in self.data:
|
|
277
|
+
return isinstance(value, (str, int, float, bool, datetime))
|
|
278
|
+
return type(self.data[key]) == type(value)
|
|
279
|
+
|
|
280
|
+
def get_key_type(self, key: str) -> type:
|
|
281
|
+
"""Get the type of a specific key."""
|
|
282
|
+
if key not in self.data:
|
|
283
|
+
raise KeyError(f"Key '{key}' not found")
|
|
284
|
+
return type(self.data[key])
|
|
285
|
+
|
|
286
|
+
def get_schema_for_key(self, key: str) -> dict:
|
|
287
|
+
"""Get JSON schema for a specific key."""
|
|
288
|
+
if key not in self.data:
|
|
289
|
+
raise KeyError(f"Key '{key}' not found")
|
|
290
|
+
|
|
291
|
+
value = self.data[key]
|
|
292
|
+
if isinstance(value, str):
|
|
293
|
+
return {"type": "string"}
|
|
294
|
+
elif isinstance(value, bool):
|
|
295
|
+
return {"type": "boolean"}
|
|
296
|
+
elif isinstance(value, int):
|
|
297
|
+
return {"type": "integer"}
|
|
298
|
+
elif isinstance(value, float):
|
|
299
|
+
return {"type": "number"}
|
|
300
|
+
elif isinstance(value, datetime):
|
|
301
|
+
return {"type": "string", "format": "date-time"}
|
|
302
|
+
else:
|
|
303
|
+
return {"type": "string"}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import shutil
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Union
|
|
6
|
+
|
|
7
|
+
from odmantic import EmbeddedModel, Field, ObjectId
|
|
8
|
+
|
|
9
|
+
from simstack.models import simstack_model
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger("file_instance")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@simstack_model
|
|
15
|
+
class FileInstance(EmbeddedModel):
|
|
16
|
+
""" """
|
|
17
|
+
|
|
18
|
+
path: str = Field(
|
|
19
|
+
description="Path to the file relative to the host work directory"
|
|
20
|
+
)
|
|
21
|
+
resource: str = Field(description="Resource name")
|
|
22
|
+
|
|
23
|
+
created_at: datetime = Field(description="Creation timestamp")
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_local_file(
|
|
27
|
+
cls, path: Union[Path, str], file_stack_id: ObjectId, make_copy: bool = True
|
|
28
|
+
):
|
|
29
|
+
"""
|
|
30
|
+
Creates a FileInstance object from a local file path.
|
|
31
|
+
|
|
32
|
+
This class method is responsible for initializing a `FileInstance` based
|
|
33
|
+
on a local file's path. It supports options to hash the file, make a
|
|
34
|
+
user-specific copy, and tracks additional metadata. The method handles
|
|
35
|
+
local file operations such as copying files to a secure directory when
|
|
36
|
+
necessary and organizes resources under a configurable working directory.
|
|
37
|
+
|
|
38
|
+
:param file_stack_id: the id of the filestack where the file is in
|
|
39
|
+
:param path: The file path to the local file. Can be either a string
|
|
40
|
+
or `Path`.
|
|
41
|
+
:param make_copy: Indicates whether a secure local copy of the file should
|
|
42
|
+
be made within the application's working directory. Defaults to True.
|
|
43
|
+
:return: A `FileInstance` object initialized with file details.
|
|
44
|
+
:rtype: FileInstance
|
|
45
|
+
:raises ValueError: If there are issues during the creation of the
|
|
46
|
+
`FileInstance` from the specified local file.
|
|
47
|
+
"""
|
|
48
|
+
source_path = path if isinstance(path, Path) else Path(path)
|
|
49
|
+
|
|
50
|
+
# Prepare the content field if in_memory is True
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
from simstack.core.context import context
|
|
54
|
+
|
|
55
|
+
if make_copy:
|
|
56
|
+
# make a local copy
|
|
57
|
+
import getpass
|
|
58
|
+
|
|
59
|
+
username = getpass.getuser()
|
|
60
|
+
relative_path = Path(username) / str(file_stack_id)
|
|
61
|
+
absolute_dir = Path(context.config.workdir) / relative_path
|
|
62
|
+
absolute_dir.mkdir(parents=True, exist_ok=True)
|
|
63
|
+
shutil.copy(path, absolute_dir)
|
|
64
|
+
else:
|
|
65
|
+
relative_path = Path(path).relative_to(context.config.workdir)
|
|
66
|
+
|
|
67
|
+
file_instance = FileInstance(
|
|
68
|
+
path=str(relative_path / source_path.name),
|
|
69
|
+
resource=context.config.resource,
|
|
70
|
+
created_at=datetime.now(),
|
|
71
|
+
)
|
|
72
|
+
return file_instance
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.error(f"Error creating FileInstance from local file {path}: {e}")
|
|
75
|
+
raise ValueError(
|
|
76
|
+
f"Could not create FileInstance from local file {path}: {e}"
|
|
77
|
+
)
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Union, List, Any, Optional
|
|
3
|
+
|
|
4
|
+
from odmantic import EmbeddedModel, Field, Model
|
|
5
|
+
|
|
6
|
+
from simstack.models import simstack_model
|
|
7
|
+
from simstack.models.files import FileStack
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FileListMixin:
|
|
11
|
+
"""
|
|
12
|
+
Mixin class containing common functionality for file list operations.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __len__(self):
|
|
16
|
+
"""
|
|
17
|
+
Returns the number of file stacks in the file list.
|
|
18
|
+
|
|
19
|
+
:return: Number of file stacks
|
|
20
|
+
:rtype: int
|
|
21
|
+
"""
|
|
22
|
+
return len(self.file_stacks)
|
|
23
|
+
|
|
24
|
+
def append(self, file_stack: FileStack):
|
|
25
|
+
"""
|
|
26
|
+
Appends a FileStack to the file list.
|
|
27
|
+
|
|
28
|
+
:param file_stack: The FileStack to append.
|
|
29
|
+
:type file_stack: FileStack
|
|
30
|
+
"""
|
|
31
|
+
self.file_stacks.append(file_stack)
|
|
32
|
+
|
|
33
|
+
def extend(self, file_stacks: Union[List[FileStack], "FileListMixin"]):
|
|
34
|
+
"""
|
|
35
|
+
Extends the file list with multiple FileStacks.
|
|
36
|
+
|
|
37
|
+
:param file_stacks: List of FileStacks or another FileListMixin to extend with
|
|
38
|
+
:type file_stacks: Union[List[FileStack], FileListMixin]
|
|
39
|
+
"""
|
|
40
|
+
if hasattr(file_stacks, "file_stacks"):
|
|
41
|
+
# It's another FileListMixin object
|
|
42
|
+
self.file_stacks.extend(file_stacks.file_stacks)
|
|
43
|
+
else:
|
|
44
|
+
# It's a list
|
|
45
|
+
self.file_stacks.extend(file_stacks)
|
|
46
|
+
|
|
47
|
+
def insert(self, index: int, file_stack: FileStack):
|
|
48
|
+
"""
|
|
49
|
+
Inserts a FileStack at the specified index.
|
|
50
|
+
|
|
51
|
+
:param index: Index where to insert the FileStack
|
|
52
|
+
:type index: int
|
|
53
|
+
:param file_stack: The FileStack to insert
|
|
54
|
+
:type file_stack: FileStack
|
|
55
|
+
"""
|
|
56
|
+
self.file_stacks.insert(index, file_stack)
|
|
57
|
+
|
|
58
|
+
def remove(self, file_stack: FileStack):
|
|
59
|
+
"""
|
|
60
|
+
Removes the first occurrence of the specified FileStack.
|
|
61
|
+
|
|
62
|
+
:param file_stack: The FileStack to remove
|
|
63
|
+
:type file_stack: FileStack
|
|
64
|
+
:raises ValueError: If the FileStack is not found
|
|
65
|
+
"""
|
|
66
|
+
self.file_stacks.remove(file_stack)
|
|
67
|
+
|
|
68
|
+
def pop(self, index: int = -1) -> FileStack:
|
|
69
|
+
"""
|
|
70
|
+
Removes and returns the FileStack at the specified index.
|
|
71
|
+
|
|
72
|
+
:param index: Index of the FileStack to remove (default is last item)
|
|
73
|
+
:type index: int
|
|
74
|
+
:return: The removed FileStack
|
|
75
|
+
:rtype: FileStack
|
|
76
|
+
:raises IndexError: If the index is out of range
|
|
77
|
+
"""
|
|
78
|
+
return self.file_stacks.pop(index)
|
|
79
|
+
|
|
80
|
+
def clear(self):
|
|
81
|
+
"""
|
|
82
|
+
Removes all FileStacks from the file list.
|
|
83
|
+
"""
|
|
84
|
+
self.file_stacks.clear()
|
|
85
|
+
|
|
86
|
+
def index(self, file_stack: FileStack, start: int = 0, stop: int = None) -> int:
|
|
87
|
+
"""
|
|
88
|
+
Returns the index of the first occurrence of the specified FileStack.
|
|
89
|
+
|
|
90
|
+
:param file_stack: The FileStack to find
|
|
91
|
+
:type file_stack: FileStack
|
|
92
|
+
:param start: Start index for search
|
|
93
|
+
:type start: int
|
|
94
|
+
:param stop: Stop index for search
|
|
95
|
+
:type stop: int
|
|
96
|
+
:return: Index of the FileStack
|
|
97
|
+
:rtype: int
|
|
98
|
+
:raises ValueError: If the FileStack is not found
|
|
99
|
+
"""
|
|
100
|
+
if stop is None:
|
|
101
|
+
return self.file_stacks.index(file_stack, start)
|
|
102
|
+
return self.file_stacks.index(file_stack, start, stop)
|
|
103
|
+
|
|
104
|
+
def count(self, file_stack: FileStack) -> int:
|
|
105
|
+
"""
|
|
106
|
+
Returns the number of occurrences of the specified FileStack.
|
|
107
|
+
|
|
108
|
+
:param file_stack: The FileStack to count
|
|
109
|
+
:type file_stack: FileStack
|
|
110
|
+
:return: Number of occurrences
|
|
111
|
+
:rtype: int
|
|
112
|
+
"""
|
|
113
|
+
return self.file_stacks.count(file_stack)
|
|
114
|
+
|
|
115
|
+
def reverse(self):
|
|
116
|
+
"""
|
|
117
|
+
Reverses the order of FileStacks in the file list.
|
|
118
|
+
"""
|
|
119
|
+
self.file_stacks.reverse()
|
|
120
|
+
|
|
121
|
+
def sort(self, key=None, reverse: bool = False):
|
|
122
|
+
"""
|
|
123
|
+
Sorts the FileStacks in the file list.
|
|
124
|
+
|
|
125
|
+
:param key: Function to extract comparison key from each FileStack
|
|
126
|
+
:param reverse: If True, sort in descending order
|
|
127
|
+
:type reverse: bool
|
|
128
|
+
"""
|
|
129
|
+
self.file_stacks.sort(key=key, reverse=reverse)
|
|
130
|
+
|
|
131
|
+
def copy(self) -> List[FileStack]:
|
|
132
|
+
"""
|
|
133
|
+
Returns a shallow copy of the file stacks list.
|
|
134
|
+
|
|
135
|
+
:return: Copy of the file stacks list
|
|
136
|
+
:rtype: List[FileStack]
|
|
137
|
+
"""
|
|
138
|
+
return self.file_stacks.copy()
|
|
139
|
+
|
|
140
|
+
def __getitem__(
|
|
141
|
+
self, index: Union[int, slice]
|
|
142
|
+
) -> Union[FileStack, List[FileStack]]:
|
|
143
|
+
"""
|
|
144
|
+
Gets FileStack(s) at the specified index or slice.
|
|
145
|
+
|
|
146
|
+
:param index: Index or slice
|
|
147
|
+
:type index: Union[int, slice]
|
|
148
|
+
:return: FileStack or list of FileStacks
|
|
149
|
+
:rtype: Union[FileStack, List[FileStack]]
|
|
150
|
+
"""
|
|
151
|
+
return self.file_stacks[index]
|
|
152
|
+
|
|
153
|
+
def __setitem__(
|
|
154
|
+
self, index: Union[int, slice], value: Union[FileStack, List[FileStack]]
|
|
155
|
+
):
|
|
156
|
+
"""
|
|
157
|
+
Sets FileStack(s) at the specified index or slice.
|
|
158
|
+
|
|
159
|
+
:param index: Index or slice
|
|
160
|
+
:type index: Union[int, slice]
|
|
161
|
+
:param value: FileStack or list of FileStacks to set
|
|
162
|
+
:type value: Union[FileStack, List[FileStack]]
|
|
163
|
+
"""
|
|
164
|
+
self.file_stacks[index] = value
|
|
165
|
+
|
|
166
|
+
def __delitem__(self, index: Union[int, slice]):
|
|
167
|
+
"""
|
|
168
|
+
Deletes FileStack(s) at the specified index or slice.
|
|
169
|
+
|
|
170
|
+
:param index: Index or slice
|
|
171
|
+
:type index: Union[int, slice]
|
|
172
|
+
"""
|
|
173
|
+
del self.file_stacks[index]
|
|
174
|
+
|
|
175
|
+
def __iter__(self):
|
|
176
|
+
"""
|
|
177
|
+
Returns an iterator over the FileStacks.
|
|
178
|
+
|
|
179
|
+
:return: Iterator over FileStacks
|
|
180
|
+
"""
|
|
181
|
+
return iter(self.file_stacks)
|
|
182
|
+
|
|
183
|
+
def __contains__(self, file_stack: FileStack) -> bool:
|
|
184
|
+
"""
|
|
185
|
+
Checks if a FileStack is in the file list.
|
|
186
|
+
|
|
187
|
+
:param file_stack: The FileStack to check for
|
|
188
|
+
:type file_stack: FileStack
|
|
189
|
+
:return: True if the FileStack is in the list, False otherwise
|
|
190
|
+
:rtype: bool
|
|
191
|
+
"""
|
|
192
|
+
return file_stack in self.file_stacks
|
|
193
|
+
|
|
194
|
+
def __bool__(self) -> bool:
|
|
195
|
+
"""
|
|
196
|
+
Returns True if the file list is not empty, False otherwise.
|
|
197
|
+
|
|
198
|
+
:return: True if file list has items, False if empty
|
|
199
|
+
:rtype: bool
|
|
200
|
+
"""
|
|
201
|
+
return bool(self.file_stacks)
|
|
202
|
+
|
|
203
|
+
def __repr__(self) -> str:
|
|
204
|
+
"""
|
|
205
|
+
Returns a string representation of the file list.
|
|
206
|
+
|
|
207
|
+
:return: String representation
|
|
208
|
+
:rtype: str
|
|
209
|
+
"""
|
|
210
|
+
return f"{self.__class__.__name__}(file_stacks={self.file_stacks!r})"
|
|
211
|
+
|
|
212
|
+
def find(self, pattern: str) -> Union[FileStack, None]:
|
|
213
|
+
"""
|
|
214
|
+
Searches through file stacks for the first file name matching the given regex pattern.
|
|
215
|
+
|
|
216
|
+
:param pattern: Regular expression pattern to match against file names
|
|
217
|
+
:type pattern: str
|
|
218
|
+
:return: First matching FileStack or None if no match found
|
|
219
|
+
:rtype: Union[FileStack, None]
|
|
220
|
+
"""
|
|
221
|
+
for file_stack in self.file_stacks:
|
|
222
|
+
if file_stack.name and re.search(pattern, file_stack.name):
|
|
223
|
+
return file_stack
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
def find_all(self, pattern: str) -> List[FileStack]:
|
|
227
|
+
"""
|
|
228
|
+
Searches through file stacks for all files with names matching the given regex pattern.
|
|
229
|
+
|
|
230
|
+
:param pattern: Regular expression pattern to match against file names
|
|
231
|
+
:type pattern: str
|
|
232
|
+
:return: List of all matching FileStacks
|
|
233
|
+
:rtype: List[FileStack]
|
|
234
|
+
"""
|
|
235
|
+
matches = []
|
|
236
|
+
for file_stack in self.file_stacks:
|
|
237
|
+
if file_stack.name and re.search(pattern, file_stack.name):
|
|
238
|
+
matches.append(file_stack)
|
|
239
|
+
return matches
|
|
240
|
+
|
|
241
|
+
def filter_by_size(
|
|
242
|
+
self, min_size: int = None, max_size: int = None
|
|
243
|
+
) -> List[FileStack]:
|
|
244
|
+
"""
|
|
245
|
+
Filters FileStacks by size range.
|
|
246
|
+
|
|
247
|
+
:param min_size: Minimum file size (inclusive)
|
|
248
|
+
:type min_size: int
|
|
249
|
+
:param max_size: Maximum file size (inclusive)
|
|
250
|
+
:type max_size: int
|
|
251
|
+
:return: List of FileStacks within the size range
|
|
252
|
+
:rtype: List[FileStack]
|
|
253
|
+
"""
|
|
254
|
+
results = []
|
|
255
|
+
for file_stack in self.file_stacks:
|
|
256
|
+
if min_size is not None and file_stack.size < min_size:
|
|
257
|
+
continue
|
|
258
|
+
if max_size is not None and file_stack.size > max_size:
|
|
259
|
+
continue
|
|
260
|
+
results.append(file_stack)
|
|
261
|
+
return results
|
|
262
|
+
|
|
263
|
+
def filter_by_property(self, property_name: str, value: Any) -> List[FileStack]:
|
|
264
|
+
"""
|
|
265
|
+
Filters FileStacks by a specific property value.
|
|
266
|
+
|
|
267
|
+
:param property_name: Name of the property to filter by
|
|
268
|
+
:type property_name: str
|
|
269
|
+
:param value: Value to match
|
|
270
|
+
:type value: Any
|
|
271
|
+
:return: List of matching FileStacks
|
|
272
|
+
:rtype: List[FileStack]
|
|
273
|
+
"""
|
|
274
|
+
results = []
|
|
275
|
+
for file_stack in self.file_stacks:
|
|
276
|
+
if (
|
|
277
|
+
hasattr(file_stack, property_name)
|
|
278
|
+
and getattr(file_stack, property_name) == value
|
|
279
|
+
):
|
|
280
|
+
results.append(file_stack)
|
|
281
|
+
return results
|
|
282
|
+
|
|
283
|
+
def sort_by_name(self, reverse: bool = False):
|
|
284
|
+
"""
|
|
285
|
+
Sorts FileStacks by name.
|
|
286
|
+
|
|
287
|
+
:param reverse: If True, sort in descending order
|
|
288
|
+
:type reverse: bool
|
|
289
|
+
"""
|
|
290
|
+
self.file_stacks.sort(key=lambda fs: fs.name or "", reverse=reverse)
|
|
291
|
+
|
|
292
|
+
def sort_by_size(self, reverse: bool = False):
|
|
293
|
+
"""
|
|
294
|
+
Sorts FileStacks by size.
|
|
295
|
+
|
|
296
|
+
:param reverse: If True, sort in descending order
|
|
297
|
+
:type reverse: bool
|
|
298
|
+
"""
|
|
299
|
+
self.file_stacks.sort(key=lambda fs: fs.size, reverse=reverse)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
@simstack_model
|
|
303
|
+
class FileList(EmbeddedModel, FileListMixin):
|
|
304
|
+
file_stacks: List[FileStack] = Field(
|
|
305
|
+
default_factory=list, description="List of file stacks"
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
@simstack_model
|
|
310
|
+
class FileListModel(Model, FileListMixin):
|
|
311
|
+
file_stacks: List[FileStack] = Field(
|
|
312
|
+
default_factory=list, description="List of file stacks"
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
@simstack_model
|
|
317
|
+
class FileListIO(Model):
|
|
318
|
+
file_list: FileList = Field(default_factory=FileList)
|
|
319
|
+
task_status: Optional[str] = None
|
|
320
|
+
error: Optional[str] = None
|
|
321
|
+
message: Optional[str] = None
|