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