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,39 @@
1
+ from datetime import datetime
2
+ from enum import Enum
3
+ from typing import Optional, List
4
+
5
+ from odmantic import Model, ObjectId, Field
6
+ from pydantic import ConfigDict
7
+
8
+ from simstack.models.parameters import Resource
9
+
10
+
11
+ class RunnerEventEnum(str, Enum):
12
+ SHUTDOWN = "shutdown"
13
+ RUNNER_STARTED = "runner_started"
14
+ NODE_STARTED = "node_started"
15
+ NODE_SUBMIT = "node_submit"
16
+ ALIVE = "alive"
17
+ RESTART = "restart"
18
+ CRONTAB_OK = "crontab_ok"
19
+ CRONTAB_GONE = "crontab_gone"
20
+
21
+
22
+ class RunnerType(str, Enum):
23
+ NODE_RUNNER = "node_runner"
24
+ RESOURCE_RUNNER = "resource_runner"
25
+
26
+
27
+ class RunnerEvent(Model):
28
+ model_config = ConfigDict(arbitrary_types_allowed=True)
29
+
30
+ timestamp: datetime = Field(default_factory=datetime.now)
31
+ event: RunnerEventEnum
32
+ resource: Resource
33
+ runner_type: RunnerType
34
+ hostname: Optional[str] = None
35
+ user: Optional[str] = None
36
+ pid: Optional[int] = None
37
+ node_id: Optional[ObjectId] = None
38
+ message: Optional[str] = None
39
+ git_status: List[str] = Field(default_factory=list)
@@ -0,0 +1,32 @@
1
+ from typing import Dict, Any, List
2
+
3
+ from odmantic import Model, Field
4
+
5
+ from simstack.models import simstack_model
6
+
7
+
8
+ @simstack_model
9
+ class SimpleTable(Model):
10
+ """
11
+ A simple table model to display tabular data using ag-grid
12
+ """
13
+
14
+ name: str = Field(default="SimpleTable")
15
+ heading: List[str] = Field(default_factory=list)
16
+ row: List[Dict[str, Any]] = Field(default_factory=list)
17
+ type: List[str] = Field(default_factory=list)
18
+
19
+ def add_column(self, column_name: str, column_type: str):
20
+ if column_name not in self.heading:
21
+ self.heading.append(column_name)
22
+ self.type.append(column_type)
23
+
24
+ def add_row(self, row: Dict[str, Any]):
25
+ self.row.append(row)
26
+
27
+ @classmethod
28
+ def ui_schema(cls) -> dict:
29
+ ui_schema = {
30
+ "ui:field": "SimpleTableField",
31
+ }
32
+ return ui_schema
@@ -0,0 +1,97 @@
1
+ from functools import wraps
2
+ from typing import TypeVar, Type, Any, get_type_hints, overload
3
+
4
+ from simstack.util.cleaned_json_schema import cleaned_json_schema
5
+ from simstack.util.custom_model_dump import custom_model_dump
6
+ from simstack.util.default_from_dict import default_from_dict, default_from_model
7
+ from simstack.util.generate_ui_schema import generate_ui_schema
8
+ from simstack.util.ui_tools import ui_make_title
9
+
10
+ T = TypeVar("T")
11
+
12
+
13
+ @overload
14
+ def simstack_model(cls: Type[T]) -> Type[T]:
15
+ ...
16
+
17
+
18
+ def simstack_model(cls: T) -> T:
19
+ """
20
+ Decorates a given class to equip it with default implementations of utility
21
+ methods for handling operations such as dictionary conversion, schema
22
+ generation, and UI schema generation.
23
+ """
24
+
25
+ # Function to create a properly typed wrapper that preserves docstrings
26
+ def create_typed_wrapper(func, first_param_name="this_class"):
27
+ # Get original type hints
28
+ original_hints = get_type_hints(func)
29
+
30
+ # Create a wrapper with the correct parameters
31
+ @wraps(func) # This preserves metadata like docstrings
32
+ def wrapper(*args, **kwargs):
33
+ return func(*args, **kwargs)
34
+
35
+ # Add explicit docstring if not preserved by wraps
36
+ if not wrapper.__doc__ and func.__doc__:
37
+ wrapper.__doc__ = func.__doc__
38
+
39
+ # Copy the original function's annotations
40
+ wrapper.__annotations__ = {
41
+ first_param_name: Type[Any], # the first param is now the class
42
+ **{
43
+ k: v
44
+ for k, v in original_hints.items()
45
+ if k != "return" and k != first_param_name
46
+ },
47
+ }
48
+
49
+ # Preserve the return annotation if it exists
50
+ if "return" in original_hints:
51
+ wrapper.__annotations__["return"] = original_hints["return"]
52
+
53
+ return wrapper
54
+
55
+ # Create typed wrappers for all functions with explicit docstrings
56
+ default_class_methods = {
57
+ "json_schema": create_typed_wrapper(cleaned_json_schema),
58
+ "ui_schema": create_typed_wrapper(generate_ui_schema),
59
+ "ui_make_title": create_typed_wrapper(ui_make_title),
60
+ "from_dict": create_typed_wrapper(default_from_dict),
61
+ "from_model": create_typed_wrapper(default_from_model),
62
+ #'make_column_defs': create_typed_wrapper(make_column_defs_helper),
63
+ }
64
+
65
+ # Add methods only if they don't exist
66
+ for method_name, default_implementation in default_class_methods.items():
67
+ if not hasattr(cls, method_name):
68
+ setattr(cls, method_name, classmethod(default_implementation))
69
+
70
+ default_methods = {
71
+ "custom_model_dump": custom_model_dump,
72
+ #'make_table_entries': make_table_entries_helper
73
+ }
74
+
75
+ # Add methods only if they don't exist
76
+ for method_name, default_implementation in default_methods.items():
77
+ if not hasattr(cls, method_name):
78
+ setattr(cls, method_name, default_implementation)
79
+
80
+ # Add a marker attribute to identify decorated classes
81
+ setattr(cls, "_is_simstack_model", True)
82
+
83
+ # Return the original class without casting - the overload handles the typing
84
+ return cls
85
+
86
+
87
+ def is_simstack_model(cls: Type) -> bool:
88
+ """
89
+ Check if a class has been decorated with @simstack_model.
90
+
91
+ Args:
92
+ cls: The class to check
93
+
94
+ Returns:
95
+ bool: True if the class was decorated with @simstack_model, False otherwise
96
+ """
97
+ return getattr(cls, "_is_simstack_model", False)
@@ -0,0 +1,17 @@
1
+ import datetime
2
+
3
+ from odmantic import Model, ObjectId
4
+
5
+ from simstack.models.parameters import Resource
6
+
7
+
8
+ class SlurmInfo(Model):
9
+ node_registry: ObjectId
10
+ updated: datetime.datetime
11
+ resource: Resource
12
+ job_id: str
13
+ name: str
14
+ user: str
15
+ code: str
16
+ time: str
17
+ nodes: list[str]
@@ -0,0 +1,87 @@
1
+ from typing import List, Optional, Dict, Any
2
+
3
+ from odmantic import Model, Field, EmbeddedModel, ObjectId
4
+
5
+
6
+ class AGGridColumnDef(EmbeddedModel):
7
+ """AG-Grid column definition with support for nested tables."""
8
+
9
+ # Core properties
10
+ field: str = Field(..., description="The field name in the row data to bind to")
11
+ headerName: str = Field(..., description="Display name for the column header")
12
+
13
+ # Sizing properties
14
+ width: Optional[int] = Field(default=None, description="Column width in pixels")
15
+ minWidth: Optional[int] = Field(
16
+ default=None, description="Minimum column width in pixels"
17
+ )
18
+ maxWidth: Optional[int] = Field(
19
+ default=None, description="Maximum column width in pixels"
20
+ )
21
+ flex: Optional[int] = Field(
22
+ default=None, description="Flex sizing for responsive columns"
23
+ )
24
+
25
+ # Display and interaction properties
26
+ hide: Optional[bool] = Field(default=None, description="Whether to hide the column")
27
+ # pinned: Optional[Literal["left", "right"]] = Field(default=None, description="Pin column to left or right")
28
+ sortable: Optional[bool] = Field(
29
+ default=None, description="Whether the column can be sorted"
30
+ )
31
+ resizable: Optional[bool] = Field(
32
+ default=None, description="Whether the column can be resized"
33
+ )
34
+ editable: Optional[bool] = Field(
35
+ default=None, description="Whether cells in this column are editable"
36
+ )
37
+
38
+ # # Filtering and rendering
39
+ # filter: Optional[Union[bool, str]] = Field(default=None, description="Filter type")
40
+ # cellRenderer: Optional[str] = Field(default=None, description="Custom cell renderer component")
41
+ # cellRendererParams: Optional[Dict[str, Any]] = Field(default=None, description="Parameters for cell renderer")
42
+
43
+ # # Master-Detail specific properties for nested tables
44
+ # cellRendererSelector: Optional[Dict[str, Any]] = Field(default=None, description="Function to select cell renderer based on data")
45
+ #
46
+ # # Nested table configuration
47
+ # is_nested_table: Optional[bool] = Field(default=False, description="Whether this column contains nested table data")
48
+ # nested_table_config: Optional[Dict[str, Any]] = Field(default=None, description="Configuration for nested table rendering")
49
+ #
50
+ # # Row grouping and detail rows
51
+ # rowGroup: Optional[bool] = Field(default=None, description="Whether this column is used for row grouping")
52
+ # showRowGroup: Optional[Union[bool, str]] = Field(default=None, description="Show row group column")
53
+ #
54
+ # # Additional properties
55
+ # custom_properties: Optional[Dict[str, Any]] = Field(default_factory=dict, description="Additional custom properties")
56
+
57
+
58
+ class TableArtifactModel(Model):
59
+ parent_id: ObjectId = Field(default=None, description="ID of the node registry")
60
+ columns_defs: List[AGGridColumnDef] = Field(
61
+ default_factory=list, description="AG-Grid column definitions"
62
+ )
63
+ row_data: List[Dict[str, Any]] = Field(
64
+ default_factory=list, description="List of row objects"
65
+ )
66
+
67
+ # Master-Detail configuration
68
+ master_detail: Optional[bool] = Field(
69
+ default=False, description="Enable master-detail rows"
70
+ )
71
+ detail_cell_renderer: Optional[str] = Field(
72
+ default=None, description="Detail panel cell renderer"
73
+ )
74
+ detail_cell_renderer_params: Optional[Dict[str, Any]] = Field(
75
+ default=None, description="Parameters for detail renderer"
76
+ )
77
+ # TODO implement nested tables and rendering
78
+ # # Nested tables storage - simplified to avoid recursion
79
+ # nested_tables: Optional[Dict[str, str]] = Field(default_factory=dict, description="Nested table IDs by reference key")
80
+ #
81
+ # # Tree data configuration
82
+ # tree_data: Optional[bool] = Field(default=False, description="Enable tree data structure")
83
+ # get_data_path: Optional[str] = Field(default=None, description="Function to get data path for tree structure")
84
+ #
85
+ # # Row grouping
86
+ # auto_group_column_def: Optional[AGGridColumnDef] = Field(default=None, description="Auto group column definition")
87
+ # group_default_expanded: Optional[int] = Field(default=None, description="Default expansion level for groups")
File without changes
@@ -0,0 +1,18 @@
1
+ async def async_zip(async_gen1, async_gen2):
2
+ """Async version of zip for two async generators"""
3
+ async for item1, item2 in async_zip_multiple(async_gen1, async_gen2):
4
+ yield item1, item2
5
+
6
+
7
+ async def async_zip_multiple(*async_generators):
8
+ """Async version of zip for multiple async generators"""
9
+ iterators = [aiter(ag) for ag in async_generators]
10
+
11
+ while True:
12
+ try:
13
+ values = []
14
+ for it in iterators:
15
+ values.append(await anext(it))
16
+ yield tuple(values)
17
+ except StopAsyncIteration:
18
+ break
@@ -0,0 +1,20 @@
1
+ def cleaned_json_schema(cls):
2
+ """
3
+
4
+ Generates a JSON schema for the given class and its fields, but eliminates
5
+ all fields which are models, embedded models, or references to models.
6
+
7
+ :param cls:
8
+ :return:
9
+ """
10
+ schema = cls.model_json_schema()
11
+ schema["title"] = cls.__name__
12
+
13
+ # for field_name, field in cls.model_fields.items():
14
+ # field_type = field.annotation
15
+ # if hasattr(field_type, '_is_simstack_model'):
16
+ # # If the field is a model or embedded model, remove its proprties
17
+ # if field_name in schema['$defs'] and "properties" in schema['$defs']['properties']:
18
+ # del schema['$defs']['properties']
19
+
20
+ return schema
@@ -0,0 +1,273 @@
1
+ import os
2
+ import sys
3
+ import tomllib
4
+ from pathlib import Path
5
+ from typing import List, Dict
6
+
7
+ from simstack.util.project_root_finder import find_project_root
8
+
9
+
10
+ class ConfigReader:
11
+ """
12
+ Handles the loading and processing of a TOML configuration file,
13
+ to retrieve relevant settings for the workflow environment.
14
+ Provides critical parameters such as database name and connection string are provided, and
15
+ allows resource-based configurations for flexibility.
16
+
17
+ :ivar config: The dictionary representation of the parsed configuration file.
18
+ :type config: dict
19
+ :ivar _resource: Specifies the resource type or scope used to determine configurations.
20
+ :type _resource: str
21
+ :ivar _db_name: The name of the database specified in the configuration.
22
+ :type _db_name: str
23
+ :ivar _connection_string: The connection string for the database specified in the configuration.
24
+ :type _connection_string: str
25
+ :ivar _workdir: The working directory based on the resource settings from the configuration.
26
+ :type _workdir: Path | None
27
+ :ivar _python_path: The Python executable path defined in the resource settings from the configuration.
28
+ :type _python_path: str | None
29
+ :ivar _environment_start: Environment-specific start command extracted from the resource settings.
30
+ :type _environment_start: str | None
31
+ """
32
+
33
+ def __init__(self, **kwargs):
34
+ config_path = (
35
+ kwargs["config_path"] if "config_path" in kwargs else find_project_root()
36
+ )
37
+ config_file = (
38
+ kwargs["config_file"] if "config_file" in kwargs else "simstack.toml"
39
+ )
40
+ try:
41
+ toml_file = os.path.join(config_path, config_file)
42
+ if os.path.exists(toml_file):
43
+ with open(toml_file, "rb") as f:
44
+ self.config = tomllib.load(f)
45
+ elif kwargs.get("is_test", False):
46
+ self.config = {
47
+ "paths": {
48
+ "tests": {"path": "tests", "drops": "", "use_pickle": False}
49
+ }
50
+ }
51
+ else:
52
+ print(f"Config file {toml_file} does not exist. Aborting.")
53
+ sys.exit(-1)
54
+
55
+ except tomllib.TOMLDecodeError:
56
+ print("There was an error decoding the TOML file.")
57
+ sys.exit(-1)
58
+
59
+ self._resource = kwargs.get("resource", "local")
60
+ self._is_test = kwargs.get("is_test", False)
61
+ self._secret_key = None
62
+ # parameter overrides config file
63
+ self._db_name = kwargs.get("db_name", None)
64
+
65
+ # for tests we can use an in_memory db
66
+ if self._db_name is None:
67
+ # the package simstack.toml has no db_name and connections string
68
+ self._db_name = (
69
+ self.config.get("parameters", {})
70
+ .get("common", {})
71
+ .get("database", "NONE")
72
+ )
73
+ if not self._is_test and self._db_name == "NONE":
74
+ print("You must specify a database name in the config file")
75
+ sys.exit(-1)
76
+
77
+ self._connection_string = kwargs.get("connection_string", None)
78
+ if self._connection_string is None:
79
+ self._connection_string = (
80
+ self.config.get("parameters", {})
81
+ .get("common", {})
82
+ .get("connection_string", "NONE")
83
+ )
84
+ if not self._is_test and self._connection_string == "NONE":
85
+ print("You must specify a connection string in the config file")
86
+ sys.exit(-1)
87
+
88
+ self._docker = False
89
+ self._workdir = None
90
+ self._external_workdir = None
91
+
92
+ self._python_path = None
93
+ self._external_source_dir = None
94
+ self._environment_start = None
95
+ self._routes = []
96
+ self._git = []
97
+
98
+ def secondary_init(self, workdir):
99
+ """
100
+ Called to perform secondary initialization after the context has been set.
101
+
102
+ This method is responsible for configuring certain parameters such as the
103
+ working directory (`workdir`), Python path (`python_path`), and the environment
104
+ start command (`environment_start`). It fetches these setups from the provided
105
+ configuration. If critical settings such as the `workdir` are not provided,
106
+ an error is logged, and a `ValueError` is raised. Logging is used throughout
107
+ to provide information about the initialization process.
108
+
109
+ :raises ValueError: If the `workdir` configuration for the resource is not specified.
110
+ """
111
+
112
+ import logging
113
+
114
+ logger = logging.getLogger("ConfigReader")
115
+
116
+ logger.info(
117
+ f"Initializing ConfigReader with resource: {self._resource} on database {self._db_name}"
118
+ )
119
+
120
+ self._docker = (
121
+ self.config.get("parameters", {})
122
+ .get(self._resource, {})
123
+ .get("docker", False)
124
+ )
125
+ logger.info(f"docker: {self._docker}")
126
+ if workdir is None:
127
+ workdir = (
128
+ self.config.get("parameters", {})
129
+ .get(self._resource, {})
130
+ .get("workdir", "NONE")
131
+ )
132
+ if workdir == "NONE":
133
+ logger.error(
134
+ f"You must specify a working directory for resource: {self._resource} in the config file"
135
+ )
136
+ raise ValueError(
137
+ f"You must specify a working directory for resource: {self._resource} in the config file"
138
+ )
139
+ self._workdir = Path(workdir)
140
+ logger.info(f"workdir: {self._workdir}")
141
+
142
+ if self._docker:
143
+ self._external_workdir = workdir
144
+ self._workdir = Path("/home/appuser/simstack")
145
+ logger.info(f"external_workdir: {self._external_workdir}")
146
+
147
+ self._external_source_dir = Path(
148
+ self.config.get("parameters", {})
149
+ .get(self._resource, {})
150
+ .get("source_dir", "NONE")
151
+ )
152
+ logger.info(f"source directory: {self._external_source_dir}")
153
+ if self.docker and self._external_source_dir == Path("NONE"):
154
+ logger.error(
155
+ f"You must specify an external source directory for resource: {self._resource} in the config file"
156
+ )
157
+ raise ValueError(
158
+ f"You must specify an external source directory for resource: {self._resource} in the config file"
159
+ )
160
+
161
+ self._python_path = (
162
+ self.config.get("parameters", {})
163
+ .get(self._resource, {})
164
+ .get("python_path", "NONE")
165
+ )
166
+ if self._python_path == "NONE":
167
+ logger.error("PYTHON PATH IS MISSING")
168
+ logger.info(f"python_path: {self._python_path}")
169
+
170
+ self._environment_start = (
171
+ self.config.get("parameters", {})
172
+ .get(self._resource, {})
173
+ .get("environment_start", "")
174
+ )
175
+ logger.info(f"environment_start: {self._environment_start}")
176
+
177
+ self._resources = (
178
+ self.config.get("parameters", {}).get("common", {}).get("resources", [])
179
+ )
180
+ logger.info(f"Initialized resources to: {self._resources}")
181
+
182
+ self._routes = self.config.get("routes", [])
183
+ for route in self._routes:
184
+ if not isinstance(route, dict):
185
+ logger.error(f"Route {route} is not a dictionary.")
186
+ raise ValueError("Route {route} is not a dictionary.")
187
+ if not ("source" in route and "target" in route and "host" in route):
188
+ logger.error(
189
+ f"Route {route} does not contain 'source', 'target', 'host' keys."
190
+ )
191
+ raise ValueError(
192
+ f"Route {route} does not contain 'source', 'target', 'host' keys."
193
+ )
194
+
195
+ self._secret_key = self.config.get("server", {}).get("secret_key", "")
196
+ self._git = (
197
+ self.config.get("parameters", {}).get(self._resource, {}).get("git", [])
198
+ )
199
+
200
+ def get_route(self, source: str, target: str) -> List[Dict[str, str]]:
201
+ """
202
+ Retrieves the route configuration for a given source and target.
203
+
204
+ :param source: The source node.
205
+ :param target: The target node.
206
+ :return: A list of dictionaries representing the route configuration.
207
+ """
208
+ for route in self._routes:
209
+ if route.get("source") == source and route.get("target") == target:
210
+ return route
211
+ return []
212
+
213
+ @property
214
+ def allowed_resources(self) -> List[str]:
215
+ return self._allowed_resources
216
+
217
+ @property
218
+ def secret_key(self) -> str:
219
+ return self._secret_key
220
+
221
+ @property
222
+ def docker(self) -> bool:
223
+ return self._docker
224
+
225
+ @property
226
+ def environment_start(self) -> str:
227
+ return self._environment_start
228
+
229
+ @property
230
+ def python_path(self) -> str:
231
+ return self._python_path
232
+
233
+ @property
234
+ def workdir(self) -> Path:
235
+ return self._workdir
236
+
237
+ @property
238
+ def external_workdir(self) -> Path:
239
+ return self._external_workdir
240
+
241
+ @property
242
+ def external_source_dir(self) -> Path:
243
+ return self._external_source_dir
244
+
245
+ @property
246
+ def git(self) -> List[Dict]:
247
+ return self._git
248
+
249
+ @property
250
+ def connection_string(self) -> str:
251
+ return self._connection_string
252
+
253
+ @property
254
+ def database_name(self) -> str:
255
+ return self._db_name
256
+
257
+ @property
258
+ def resource(self) -> str:
259
+ return self._resource
260
+
261
+ @resource.setter
262
+ def resource(self, value: str):
263
+ self._resource = value
264
+
265
+ @property
266
+ def paths(self) -> Dict:
267
+ """
268
+ Get the path configuration from the TOML file.
269
+
270
+ Returns:
271
+ Dictionary containing path configurations or an empty dict if not found
272
+ """
273
+ return self.config.get("paths", {})
@@ -0,0 +1,75 @@
1
+ from typing import Dict, Any
2
+
3
+
4
+ async def custom_model_dump(self, **kwargs) -> Dict[str, Any]:
5
+ """
6
+ Custom model dump method to handle the conversion of model instances to dictionaries.
7
+ This method recursively traverses dictionaries and lists to convert any nested
8
+ model instances to their dictionary representation.
9
+
10
+ :param self: The model instance
11
+ :param kwargs: Additional keyword arguments
12
+ :return: A dictionary representation of the model instance
13
+ """
14
+ # First get the dictionary from the standard model_dump
15
+
16
+ filtered_kwargs = {k: v for k, v in kwargs.items() if k != "engine"}
17
+ data = self.model_dump(**filtered_kwargs)
18
+
19
+ # Process each field - the key is to process the original values from self
20
+ for field_name, field in self.model_fields.items():
21
+ field_value = getattr(self, field_name)
22
+
23
+ # Skip None values
24
+ if field_value is None:
25
+ continue
26
+
27
+ data[field_name] = await _recursive_process(field_value, **kwargs)
28
+
29
+ return data
30
+
31
+
32
+ async def _recursive_process(obj, **kwargs):
33
+ """
34
+ Helper function to recursively process an object that might contain models
35
+ """
36
+ if hasattr(obj, "custom_model_dump"):
37
+ return await obj.custom_model_dump(**kwargs)
38
+ elif isinstance(obj, dict):
39
+ return {k: await _recursive_process(v, **kwargs) for k, v in obj.items()}
40
+ elif isinstance(obj, list):
41
+ return [await _recursive_process(item, **kwargs) for item in obj]
42
+ else:
43
+ return obj
44
+
45
+
46
+ async def _process_container(container, **kwargs):
47
+ """
48
+ Helper function to recursively process dictionaries and lists,
49
+ converting any model instances to their dictionary representation.
50
+
51
+ :param container: The container to process (dict or list)
52
+ :param kwargs: Additional keyword arguments
53
+ :return: Processed container with model instances converted to dictionaries
54
+ """
55
+ if isinstance(container, dict):
56
+ result = {}
57
+ for k, v in container.items():
58
+ if hasattr(v, "custom_model_dump"):
59
+ result[k] = await v.custom_model_dump(**kwargs)
60
+ elif isinstance(v, (dict, list)):
61
+ result[k] = await _process_container(v, **kwargs)
62
+ else:
63
+ result[k] = v
64
+ return result
65
+ elif isinstance(container, list):
66
+ result = []
67
+ for item in container:
68
+ if hasattr(item, "custom_model_dump"):
69
+ result.append(await item.custom_model_dump(**kwargs))
70
+ elif isinstance(item, (dict, list)):
71
+ result.append(await _process_container(item, **kwargs))
72
+ else:
73
+ result.append(item)
74
+ return result
75
+ return container