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,33 @@
1
+ import os
2
+ from typing import List
3
+
4
+
5
+ def find_python_files(directory: str) -> List[str]:
6
+ """
7
+ Returns full paths of all .py files in the given directory and its subdirectories,
8
+ excluding __init__.py files.
9
+
10
+ Args:
11
+ directory: The root directory to search from
12
+
13
+ Returns:
14
+ List of absolute file paths to all .py files that aren't __init__.py
15
+ """
16
+ python_files = []
17
+
18
+ # Ensure the directory exists
19
+ if not os.path.isdir(directory):
20
+ raise ValueError(f"'{directory}' is not a valid directory")
21
+
22
+ # Convert to absolute path
23
+ directory = os.path.abspath(directory)
24
+
25
+ # Walk through directory structure
26
+ for root, dirs, files in os.walk(directory):
27
+ for file in files:
28
+ # Check if it's a Python file but not __init__.py
29
+ if file.endswith(".py") and file != "__init__.py":
30
+ full_path = os.path.join(root, file)
31
+ python_files.append(full_path)
32
+
33
+ return python_files
@@ -0,0 +1,80 @@
1
+ from typing import Union
2
+
3
+
4
+ def generate_ui_schema(cls):
5
+ """
6
+ Generates a UI schema that uses GenericForm for fields with ui_schema function.
7
+ Also preserves any existing UI schema configurations from the class.
8
+
9
+ Args:
10
+ cls: The model class to generate UI schema for
11
+
12
+ Returns:
13
+ dict: The generated UI schema
14
+ """
15
+ # Get base UI schema from the class if it exists
16
+
17
+ base_schema = cls.ui_base_schema() if hasattr(cls, "ui_base_schema") else {}
18
+
19
+ # Process each field in the model
20
+ for field_name, field in cls.model_fields.items():
21
+ field_type = field.annotation
22
+
23
+ # Unwrap Optional type to get the underlying type
24
+ if hasattr(field_type, "__origin__") and field_type.__origin__ is Union:
25
+ # Optional[X] is implemented as Union[X, None]
26
+ args = field_type.__args__
27
+ if len(args) == 2 and args[1] is type(None):
28
+ field_type = args[0]
29
+
30
+ # Handle Lists by checking their inner type
31
+ is_list = hasattr(field_type, "__origin__") and field_type.__origin__ is list
32
+ if is_list:
33
+ field_type = field_type.__args__[0]
34
+
35
+ # Check for Optional inside List
36
+ if hasattr(field_type, "__origin__") and field_type.__origin__ is Union:
37
+ args = field_type.__args__
38
+ if len(args) == 2 and args[1] is type(None):
39
+ field_type = args[0]
40
+
41
+ # Check if field type has ui_schema method (this is true for all simstack_model classes)
42
+ if hasattr(field_type, "ui_schema"):
43
+ module_path = field_type.__module__
44
+ full_path = f"{module_path}.{field_type.__name__}"
45
+ # if simstack is in the module path, skip all elements after the last simstack occurrence
46
+ if "simstack" in module_path:
47
+ full_path = ".".join(
48
+ module_path[module_path.rfind("simstack") :].split(".")
49
+ + [field_type.__name__]
50
+ )
51
+
52
+ field_schema = field_type.ui_schema() # get the field schema for the field
53
+ # Don't override if there's already a UI configuration for this field
54
+ if field_name not in base_schema:
55
+ if "ui:field" in field_schema:
56
+ # If the field schema already has a ui:field, use it
57
+ if is_list:
58
+ base_schema[field_name] = {"items": field_schema}
59
+ else:
60
+ base_schema[field_name] = field_schema
61
+ else:
62
+ # Check if field type is a list
63
+ ui_ref = {
64
+ "ui:field": "GenericFormField",
65
+ "ui:options": {
66
+ "model": full_path,
67
+ },
68
+ }
69
+ # in rjsf the ui_scheme for list element must be in "items" key
70
+ if is_list:
71
+ base_schema[field_name] = {"items": ui_ref}
72
+ else:
73
+ ui_ref["ui:options"]["accordion"] = "true"
74
+ base_schema[field_name] = ui_ref
75
+
76
+ # Always hide the id field if it exists
77
+ if "id" not in base_schema:
78
+ base_schema["id"] = {"ui:widget": "hidden"}
79
+
80
+ return base_schema
@@ -0,0 +1,118 @@
1
+ import math
2
+ from typing import List
3
+
4
+
5
+ def _normalize_vector(v: List[float]) -> List[float]:
6
+ """Normalize a vector to unit length."""
7
+ norm = math.sqrt(sum(x * x for x in v))
8
+ if norm < 1e-10:
9
+ return [1.0, 0.0, 0.0] # Default direction for zero vector
10
+ return [x / norm for x in v]
11
+
12
+
13
+ def _cross_product(a: List[float], b: List[float]) -> List[float]:
14
+ """Calculate cross product of two 3D vectors."""
15
+ return [
16
+ a[1] * b[2] - a[2] * b[1],
17
+ a[2] * b[0] - a[0] * b[2],
18
+ a[0] * b[1] - a[1] * b[0],
19
+ ]
20
+
21
+
22
+ def _get_perpendicular_direction(v1: List[float]) -> List[float]:
23
+ """Get a direction perpendicular to v1."""
24
+ # Find the component with smallest absolute value
25
+ min_idx = min(range(3), key=lambda i: abs(v1[i]))
26
+
27
+ # Create perpendicular vector
28
+ v2 = [0.0, 0.0, 0.0]
29
+ v2[min_idx] = 1.0
30
+
31
+ # Make it perpendicular using Gram-Schmidt
32
+ dot_product = sum(v1[i] * v2[i] for i in range(3))
33
+ for i in range(3):
34
+ v2[i] -= dot_product * v1[i]
35
+
36
+ return v2
37
+
38
+
39
+ def _get_principal_direction(matrix: List[List[float]]) -> List[float]:
40
+ """Get the principal direction (largest eigenvalue direction) using power iteration."""
41
+ # Start with random vector
42
+ v = [1.0, 1.0, 1.0]
43
+
44
+ # Power iteration
45
+ for _ in range(10): # Usually converges quickly
46
+ # v = matrix * v
47
+ new_v = [0.0, 0.0, 0.0]
48
+ for i in range(3):
49
+ for j in range(3):
50
+ new_v[i] += matrix[i][j] * v[j]
51
+
52
+ # Normalize
53
+ norm = math.sqrt(sum(x * x for x in new_v))
54
+ if norm > 1e-10:
55
+ v = [x / norm for x in new_v]
56
+
57
+ return v
58
+
59
+
60
+ def _get_eigenvectors_3x3(matrix: List[List[float]]) -> List[List[float]]:
61
+ """
62
+ Get eigenvectors of a 3x3 matrix using simplified approach.
63
+ For the inertia tensor, we'll use a simplified canonical form.
64
+ """
65
+ # For molecular hashing, we can use a simplified approach
66
+ # that still provides rotation invariance
67
+
68
+ # Create an orthonormal basis from the inertia tensor
69
+ # Start with the direction of maximum variance
70
+ v1 = _get_principal_direction(matrix)
71
+
72
+ # Get a perpendicular direction
73
+ v2 = _get_perpendicular_direction(v1)
74
+
75
+ # Third direction is cross product
76
+ v3 = _cross_product(v1, v2)
77
+
78
+ # Normalize all vectors
79
+ v1 = _normalize_vector(v1)
80
+ v2 = _normalize_vector(v2)
81
+ v3 = _normalize_vector(v3)
82
+
83
+ # Return as columns of transformation matrix
84
+ return [[v1[0], v2[0], v3[0]], [v1[1], v2[1], v3[1]], [v1[2], v2[2], v3[2]]]
85
+
86
+
87
+ def _get_canonical_orientation(coords: List[List[float]]) -> List[List[float]]:
88
+ """
89
+ Get coordinates in a canonical orientation using the inertia tensor.
90
+
91
+ :param coords: Centered coordinates of atoms
92
+ :return: Coordinates in canonical orientation
93
+ """
94
+ if len(coords) < 2:
95
+ return coords
96
+
97
+ # Calculate inertia tensor (3x3 matrix)
98
+ inertia_tensor = [[0.0, 0.0, 0.0], [0.0, 0.0, 0.0], [0.0, 0.0, 0.0]]
99
+
100
+ for pos in coords:
101
+ # I_ij = sum(r_k * r_l) where k,l are coordinate indices
102
+ for i in range(3):
103
+ for j in range(3):
104
+ inertia_tensor[i][j] += pos[i] * pos[j]
105
+
106
+ # Get eigenvalues and eigenvectors using power iteration method
107
+ eigenvectors = _get_eigenvectors_3x3(inertia_tensor)
108
+
109
+ # Transform coordinates to canonical orientation
110
+ canonical_coords = []
111
+ for coord in coords:
112
+ new_coord = [0.0, 0.0, 0.0]
113
+ for i in range(3):
114
+ for j in range(3):
115
+ new_coord[i] += coord[j] * eigenvectors[j][i]
116
+ canonical_coords.append(new_coord)
117
+
118
+ return canonical_coords
@@ -0,0 +1,26 @@
1
+ import os
2
+
3
+ from simstack.util.project_root_finder import find_project_root
4
+
5
+
6
+ # Get the module name from the current file path
7
+ def get_module_path(file_path: str):
8
+ # Get the absolute path of the current file
9
+ file_path = os.path.abspath(file_path)
10
+
11
+ # # Get all directories in the Python path
12
+ # for path in sys.path:
13
+ # if path and file_path.startswith(path):
14
+ # # Remove the path prefix and the .py extension
15
+ # relative_path = file_path[len(path):].lstrip(os.sep)
16
+ # module_path = os.path.splitext(relative_path)[0].replace(os.sep, '.')
17
+ # return module_path
18
+
19
+ # If not found in sys.path, use an alternative approach
20
+ project_root = find_project_root() # Assumes running from project root
21
+ if file_path.startswith(project_root):
22
+ relative_path = file_path[len(project_root) :].lstrip(os.sep)
23
+ module_path = os.path.splitext(relative_path)[0].replace(os.sep, ".")
24
+ return module_path
25
+
26
+ return None
@@ -0,0 +1,65 @@
1
+ import logging
2
+ import os
3
+ import subprocess
4
+ import time
5
+ from datetime import datetime
6
+
7
+ logger = logging.getLogger("git_pull")
8
+
9
+
10
+ def git_pull_periodically(repo_path, interval_minutes=1, log_file=None):
11
+ """
12
+ Performs git pull in the specified repository at regular intervals.
13
+
14
+ Args:
15
+ repo_path (str): Path to the Git repository
16
+ interval_minutes (int): Time interval between pulls in minutes
17
+ log_file (str, optional): Path to log file. If None, logs to console.
18
+
19
+ Returns:
20
+ None
21
+ """
22
+
23
+ # Convert minutes to seconds
24
+ interval_seconds = interval_minutes * 60
25
+ logger.info("git_pull " + repo_path)
26
+ # Check if the path is a valid git repository
27
+ if not os.path.exists(os.path.join(repo_path, ".git")):
28
+ logging.error(f"The directory {repo_path} is not a valid Git repository.")
29
+ return
30
+
31
+ logging.info(
32
+ f"Starting periodic git pull every {interval_minutes} minutes for {repo_path}"
33
+ )
34
+
35
+ try:
36
+ while True:
37
+ try:
38
+ # Change to the repository directory
39
+ os.chdir(repo_path)
40
+
41
+ # Log the start time
42
+ now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
43
+ logging.info(f"Executing git pull at {now}")
44
+
45
+ # Execute git pull
46
+ result = subprocess.run(
47
+ ["git", "pull"], capture_output=True, text=True, check=True
48
+ )
49
+
50
+ # Log the output
51
+ logging.info(f"Git pull output: {result.stdout.strip()}")
52
+ if result.stderr:
53
+ logging.warning(f"Git pull stderr: {result.stderr.strip()}")
54
+
55
+ except subprocess.CalledProcessError as e:
56
+ logging.error(f"Git pull failed: {e.stderr.strip()}")
57
+ except Exception as e:
58
+ logging.error(f"Error during git pull: {str(e)}")
59
+
60
+ # Wait for the next interval
61
+ logging.info(f"Waiting {interval_minutes} minutes until next pull...")
62
+ time.sleep(interval_seconds)
63
+
64
+ except KeyboardInterrupt:
65
+ logging.info("Git pull process stopped by user.")
@@ -0,0 +1,93 @@
1
+ # Python
2
+ import subprocess
3
+ from pathlib import Path
4
+ from typing import Optional, Dict
5
+
6
+
7
+ def get_git_status(repo_path: Path) -> Dict[str, Optional[object]]:
8
+ """
9
+ Get the current Git short commit hash and whether the branch is up to date with its upstream.
10
+
11
+ Returns a dict:
12
+ {
13
+ "short_hash": Optional[str], # e.g., "a1b2c3d", None if not a git repo
14
+ "branch": Optional[str], # current branch name or "HEAD" if detached, None if not a repo
15
+ "up_to_date": Optional[bool], # True/False if upstream exists, None if unknown/no upstream
16
+ "ahead": Optional[int], # commits ahead of upstream (0 if up to date), None if unknown
17
+ "behind": Optional[int], # commits behind upstream (0 if up to date), None if unknown
18
+ }
19
+ """
20
+
21
+ def run_git(args, check=False):
22
+ return subprocess.run(
23
+ ["git", "-C", str(repo_path), *args],
24
+ capture_output=True,
25
+ text=True,
26
+ check=check,
27
+ )
28
+
29
+ result: Dict[str, Optional[object]] = {
30
+ "short_hash": None,
31
+ "branch": None,
32
+ "up_to_date": None,
33
+ "ahead": None,
34
+ "behind": None,
35
+ }
36
+
37
+ # Verify we're in a git repository
38
+ try:
39
+ inside = run_git(["rev-parse", "--is-inside-work-tree"])
40
+ if inside.returncode != 0 or inside.stdout.strip() != "true":
41
+ return result # Not a repository
42
+ except Exception:
43
+ return result
44
+
45
+ # Get short hash
46
+ try:
47
+ hash_res = run_git(["rev-parse", "--short", "HEAD"], check=True)
48
+ result["short_hash"] = hash_res.stdout.strip()
49
+ except subprocess.CalledProcessError:
50
+ # Detached or other state where HEAD isn't resolvable
51
+ result["short_hash"] = None
52
+
53
+ # Get branch (or "HEAD" if detached)
54
+ try:
55
+ branch_res = run_git(["rev-parse", "--abbrev-ref", "HEAD"], check=True)
56
+ result["branch"] = branch_res.stdout.strip()
57
+ except subprocess.CalledProcessError:
58
+ result["branch"] = None
59
+
60
+ # Find upstream; if none, we can't determine up-to-date status against a remote
61
+ try:
62
+ upstream_res = run_git(
63
+ ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{upstream}"]
64
+ )
65
+ if upstream_res.returncode != 0:
66
+ # No upstream configured
67
+ return result
68
+
69
+ upstream_ref = upstream_res.stdout.strip()
70
+
71
+ # Ensure we compare against the latest remote state
72
+ run_git(["fetch", "--quiet"])
73
+
74
+ # Compute ahead/behind relative to upstream
75
+ # Format: "<behind> <ahead>" because left is upstream, right is HEAD
76
+ counts_res = run_git(
77
+ ["rev-list", "--left-right", "--count", f"{upstream_ref}...HEAD"],
78
+ check=True,
79
+ )
80
+ behind_str, ahead_str = counts_res.stdout.strip().split()
81
+ behind, ahead = int(behind_str), int(ahead_str)
82
+
83
+ result["behind"] = behind
84
+ result["ahead"] = ahead
85
+ result["up_to_date"] = behind == 0 and ahead == 0
86
+ except subprocess.CalledProcessError:
87
+ # If comparison fails, leave up_to_date/ahead/behind as None
88
+ pass
89
+ except ValueError:
90
+ # Unexpected parsing format
91
+ pass
92
+
93
+ return result
@@ -0,0 +1,65 @@
1
+ import importlib
2
+ import logging
3
+ import os
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from simstack.util.project_root_finder import find_project_root
8
+
9
+ logger = logging.getLogger("import_module_from_file")
10
+
11
+
12
+ def import_module_from_file(file_path: Path):
13
+ """
14
+ Import a Python file as a module.
15
+
16
+ Args:
17
+ file_path: Path object pointing to the Python file to import
18
+
19
+ Returns:
20
+ Imported module or None if import failed
21
+ """
22
+ try:
23
+ logger.debug(f"Attempting to import module from: {file_path}")
24
+ if not file_path.exists():
25
+ print(f"File not found: {file_path}")
26
+ return []
27
+
28
+ root_dir = find_project_root()
29
+ relative_path = file_path.relative_to(root_dir)
30
+ directory, filename = os.path.split(str(relative_path))
31
+ basename = filename.split(".")[0]
32
+
33
+ module_name = ".".join(relative_path.parts[:-1]) + "." + basename
34
+
35
+ if root_dir not in sys.path:
36
+ sys.path.insert(0, root_dir)
37
+ logger.debug(f"Added {root_dir} to sys.path")
38
+ # Try simple import first
39
+ try:
40
+ module = importlib.import_module(module_name)
41
+ return module
42
+ except ImportError as e:
43
+ logger.error(f"Direct import failed: {e}")
44
+
45
+ # Fall back to spec-based import
46
+ spec = importlib.util.spec_from_file_location(module_name, str(file_path))
47
+ if spec is None or spec.loader is None:
48
+ print(f"Failed to create spec for {file_path}")
49
+ return None
50
+
51
+ module = importlib.util.module_from_spec(spec)
52
+ sys.modules[module_name] = module
53
+ try:
54
+ spec.loader.exec_module(module)
55
+ return module
56
+ except Exception as e:
57
+ logger.error(f"Error processing module: {file_path} {e}")
58
+ return None
59
+
60
+ except Exception as e:
61
+ logger.error(f"Error importing module from {file_path}: {e}")
62
+ import traceback
63
+
64
+ traceback.print_exc()
65
+ return None
@@ -0,0 +1,163 @@
1
+ import importlib
2
+ import logging
3
+ from typing import Callable, Optional, Type
4
+
5
+ from odmantic import Model, AIOEngine, ObjectId
6
+
7
+ from simstack.core.context import context
8
+ from simstack.core.engine import current_engine_context
9
+ from simstack.models.models import ModelMapping, NodeModel
10
+
11
+ logger = logging.getLogger("importer")
12
+
13
+
14
+ async def function_from_model(model, task_id: ObjectId) -> Optional[Callable]:
15
+ function_path = model.function_mapping
16
+ module_path, function_name = function_path.rsplit(".", 1)
17
+
18
+ # if model.pickle_function is not None:
19
+ # logger.info(f"task_id: {task_id} found pickle_function for {function_path}")
20
+ # # The pickle_function is a reference to the FunctionPickle
21
+ # function_pickle = model.pickle_function
22
+ # logger.info(f"task_id: {task_id} loading function {function_path} from database")
23
+ # # Load the function from the FunctionPickle
24
+ # func = function_pickle.load_function()
25
+ # logger.info(f"task_id: {task_id} Signature: {inspect.signature(func)} _inner: {hasattr(func, "_inner")}")
26
+ #
27
+ # # Safe source code retrieval for pickled functions
28
+ # try:
29
+ # source_code = inspect.getsource(func)
30
+ # logger.info(f"Source: {source_code}")
31
+ # except (OSError, TypeError):
32
+ # logger.info("Source code not available (function loaded from pickle)")
33
+ #
34
+ # return func
35
+ # else:
36
+
37
+ logger.info(
38
+ f"task_id: {task_id} loading function {function_path} using regular import"
39
+ )
40
+ # Import the module
41
+ module = importlib.import_module(module_path)
42
+ # Get the function from the module
43
+ function = getattr(module, function_name)
44
+ return function
45
+
46
+
47
+ async def import_function(
48
+ function_path: str, task_id: ObjectId = None
49
+ ) -> Optional[Callable]:
50
+ """
51
+ Dynamically import a function from a module using its full path.
52
+ load the function information using NodeModel
53
+ load the pickled version if it exists
54
+ if there is no pickled version, use regular import.
55
+
56
+ Args:
57
+ function_path: Dot notation path to the function (e.g. 'methods.submodule.function_name')
58
+ task_id: Optional task Id
59
+
60
+ Returns:
61
+ The imported function object or None if import fails
62
+ """
63
+ engine = current_engine_context.get()
64
+
65
+ node_model = await engine.find_one(
66
+ NodeModel, NodeModel.function_mapping == function_path
67
+ )
68
+ if node_model is None:
69
+ raise LookupError(
70
+ f"task_id: {task_id} Function {function_path} not found in the NodeModel Table"
71
+ )
72
+
73
+ return await function_from_model(node_model, task_id)
74
+
75
+
76
+ async def import_function_by_name(
77
+ function_name: str, task_id: ObjectId, engine: AIOEngine = None
78
+ ) -> Optional[Callable]:
79
+ if not engine:
80
+ engine = context.db.engine
81
+
82
+ node_model = await engine.find_one(NodeModel, NodeModel.name == function_name)
83
+ if node_model is None:
84
+ logger.error(f"Could not find function mapping for name: {function_name}")
85
+ raise ValueError(f"Could not find function mapping for name: {function_name}")
86
+
87
+ return await function_from_model(node_model, task_id)
88
+
89
+
90
+ async def import_class(class_path: str) -> Type[Model] | None:
91
+ """
92
+ Dynamically import a class from a module using its full path.
93
+ First tries to load the class from the database using ModelMapping
94
+
95
+ A pickled version of the class is used primarily
96
+
97
+
98
+ Args:
99
+ class_path: Dot notation path to the class (e.g. 'models.submodule.ClassName')
100
+ :param class_path:
101
+ Returns:
102
+ The imported class object or None if import fails
103
+ """
104
+
105
+ try:
106
+ engine = current_engine_context.get()
107
+ # Split the path into module path and class name
108
+ module_path, class_name = class_path.rsplit(".", 1)
109
+ model_mapping = await engine.find_one(
110
+ ModelMapping, ModelMapping.name == class_name
111
+ )
112
+
113
+ # If not found by name, try by mapping
114
+ if not model_mapping:
115
+ model_mapping = await engine.find_one(
116
+ ModelMapping, ModelMapping.mapping == class_path
117
+ )
118
+ else: # when searching by name the path may have changed
119
+ module_path, class_name = model_mapping.mapping.rsplit(".", 1)
120
+
121
+ if not model_mapping:
122
+ logger.error(f"Error finding ModelMapping for {class_name}")
123
+ raise LookupError(f"Error finding ModelMapping for {class_name}")
124
+
125
+ # TODO where do picke classes come from?
126
+ # If we found a ModelMapping with a pickle_class reference, try to load from the database
127
+ # if model_mapping.pickle_class:
128
+ # logger.info(f"Found ModelMapping for {class_name} with pickle_class")
129
+ # try:
130
+ # # The pickle_class is a reference to the ClassPickle
131
+ # class_pickle = model_mapping.pickle_class
132
+ # if class_pickle:
133
+ # logger.info(f"Loading class {class_name} from database")
134
+ # # Load the class from the ClassPickle
135
+ # pickle_result = cast(Type[Model], class_pickle.load_class())
136
+ # return pickle_result
137
+ # else:
138
+ # logger.warning(f"ClassPickle not found for {class_name}")
139
+ # raise LookupError(f"ClassPickle not found for {class_name}")
140
+ # except Exception as e:
141
+ # logger.error(f"Error loading class {class_name} from database: {e}")
142
+ # raise e
143
+ # else:
144
+
145
+ # Import the module
146
+ module = importlib.import_module(module_path)
147
+
148
+ # Get the class from the module
149
+ return getattr(module, class_name)
150
+ except (ImportError, AttributeError, ValueError) as e:
151
+ logger.error(f"Error importing class {class_path}: {e}")
152
+ raise e
153
+
154
+
155
+ async def import_class_by_name(class_name: str) -> Type[Model]:
156
+ engine = current_engine_context.get()
157
+ model_mapping = await engine.find_one(ModelMapping, ModelMapping.name == class_name)
158
+
159
+ if not model_mapping:
160
+ logger.error(f"Error finding ModelMapping for {class_name}")
161
+ raise LookupError(f"Error finding ModelMapping for {class_name}")
162
+
163
+ return await import_class(model_mapping.mapping)