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,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