synapse-sdk 1.0.0b5__py3-none-any.whl → 2025.12.3__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 (167) hide show
  1. synapse_sdk/__init__.py +24 -0
  2. synapse_sdk/cli/code_server.py +305 -33
  3. synapse_sdk/clients/agent/__init__.py +2 -1
  4. synapse_sdk/clients/agent/container.py +143 -0
  5. synapse_sdk/clients/agent/ray.py +296 -38
  6. synapse_sdk/clients/backend/annotation.py +1 -1
  7. synapse_sdk/clients/backend/core.py +31 -4
  8. synapse_sdk/clients/backend/data_collection.py +82 -7
  9. synapse_sdk/clients/backend/hitl.py +1 -1
  10. synapse_sdk/clients/backend/ml.py +1 -1
  11. synapse_sdk/clients/base.py +211 -61
  12. synapse_sdk/loggers.py +46 -0
  13. synapse_sdk/plugins/README.md +1340 -0
  14. synapse_sdk/plugins/categories/base.py +59 -9
  15. synapse_sdk/plugins/categories/export/actions/__init__.py +3 -0
  16. synapse_sdk/plugins/categories/export/actions/export/__init__.py +28 -0
  17. synapse_sdk/plugins/categories/export/actions/export/action.py +165 -0
  18. synapse_sdk/plugins/categories/export/actions/export/enums.py +113 -0
  19. synapse_sdk/plugins/categories/export/actions/export/exceptions.py +53 -0
  20. synapse_sdk/plugins/categories/export/actions/export/models.py +74 -0
  21. synapse_sdk/plugins/categories/export/actions/export/run.py +195 -0
  22. synapse_sdk/plugins/categories/export/actions/export/utils.py +187 -0
  23. synapse_sdk/plugins/categories/export/templates/config.yaml +19 -1
  24. synapse_sdk/plugins/categories/export/templates/plugin/__init__.py +390 -0
  25. synapse_sdk/plugins/categories/export/templates/plugin/export.py +153 -177
  26. synapse_sdk/plugins/categories/neural_net/actions/train.py +1130 -32
  27. synapse_sdk/plugins/categories/neural_net/actions/tune.py +157 -4
  28. synapse_sdk/plugins/categories/neural_net/templates/config.yaml +7 -4
  29. synapse_sdk/plugins/categories/pre_annotation/actions/__init__.py +4 -0
  30. synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/__init__.py +3 -0
  31. synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/action.py +10 -0
  32. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/__init__.py +28 -0
  33. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/action.py +148 -0
  34. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/enums.py +269 -0
  35. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/exceptions.py +14 -0
  36. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/factory.py +76 -0
  37. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/models.py +100 -0
  38. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/orchestrator.py +248 -0
  39. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/run.py +64 -0
  40. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/__init__.py +17 -0
  41. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/annotation.py +265 -0
  42. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/base.py +170 -0
  43. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/extraction.py +83 -0
  44. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/metrics.py +92 -0
  45. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/preprocessor.py +243 -0
  46. synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/validation.py +143 -0
  47. synapse_sdk/plugins/categories/upload/actions/upload/__init__.py +19 -0
  48. synapse_sdk/plugins/categories/upload/actions/upload/action.py +236 -0
  49. synapse_sdk/plugins/categories/upload/actions/upload/context.py +185 -0
  50. synapse_sdk/plugins/categories/upload/actions/upload/enums.py +493 -0
  51. synapse_sdk/plugins/categories/upload/actions/upload/exceptions.py +36 -0
  52. synapse_sdk/plugins/categories/upload/actions/upload/factory.py +138 -0
  53. synapse_sdk/plugins/categories/upload/actions/upload/models.py +214 -0
  54. synapse_sdk/plugins/categories/upload/actions/upload/orchestrator.py +183 -0
  55. synapse_sdk/plugins/categories/upload/actions/upload/registry.py +113 -0
  56. synapse_sdk/plugins/categories/upload/actions/upload/run.py +179 -0
  57. synapse_sdk/plugins/categories/upload/actions/upload/steps/__init__.py +1 -0
  58. synapse_sdk/plugins/categories/upload/actions/upload/steps/base.py +107 -0
  59. synapse_sdk/plugins/categories/upload/actions/upload/steps/cleanup.py +62 -0
  60. synapse_sdk/plugins/categories/upload/actions/upload/steps/collection.py +63 -0
  61. synapse_sdk/plugins/categories/upload/actions/upload/steps/generate.py +91 -0
  62. synapse_sdk/plugins/categories/upload/actions/upload/steps/initialize.py +82 -0
  63. synapse_sdk/plugins/categories/upload/actions/upload/steps/metadata.py +235 -0
  64. synapse_sdk/plugins/categories/upload/actions/upload/steps/organize.py +201 -0
  65. synapse_sdk/plugins/categories/upload/actions/upload/steps/upload.py +104 -0
  66. synapse_sdk/plugins/categories/upload/actions/upload/steps/validate.py +71 -0
  67. synapse_sdk/plugins/categories/upload/actions/upload/strategies/__init__.py +1 -0
  68. synapse_sdk/plugins/categories/upload/actions/upload/strategies/base.py +82 -0
  69. synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/__init__.py +1 -0
  70. synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/batch.py +39 -0
  71. synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/single.py +29 -0
  72. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/__init__.py +1 -0
  73. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/flat.py +300 -0
  74. synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/recursive.py +287 -0
  75. synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/__init__.py +1 -0
  76. synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/excel.py +174 -0
  77. synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/none.py +16 -0
  78. synapse_sdk/plugins/categories/upload/actions/upload/strategies/upload/__init__.py +1 -0
  79. synapse_sdk/plugins/categories/upload/actions/upload/strategies/upload/sync.py +84 -0
  80. synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/__init__.py +1 -0
  81. synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/default.py +60 -0
  82. synapse_sdk/plugins/categories/upload/actions/upload/utils.py +250 -0
  83. synapse_sdk/plugins/categories/upload/templates/README.md +470 -0
  84. synapse_sdk/plugins/categories/upload/templates/config.yaml +28 -2
  85. synapse_sdk/plugins/categories/upload/templates/plugin/__init__.py +310 -0
  86. synapse_sdk/plugins/categories/upload/templates/plugin/upload.py +82 -20
  87. synapse_sdk/plugins/models.py +111 -9
  88. synapse_sdk/plugins/templates/plugin-config-schema.json +7 -0
  89. synapse_sdk/plugins/templates/schema.json +7 -0
  90. synapse_sdk/plugins/utils/__init__.py +3 -0
  91. synapse_sdk/plugins/utils/ray_gcs.py +66 -0
  92. synapse_sdk/shared/__init__.py +25 -0
  93. synapse_sdk/utils/converters/dm/__init__.py +42 -41
  94. synapse_sdk/utils/converters/dm/base.py +137 -0
  95. synapse_sdk/utils/converters/dm/from_v1.py +208 -562
  96. synapse_sdk/utils/converters/dm/to_v1.py +258 -304
  97. synapse_sdk/utils/converters/dm/tools/__init__.py +214 -0
  98. synapse_sdk/utils/converters/dm/tools/answer.py +95 -0
  99. synapse_sdk/utils/converters/dm/tools/bounding_box.py +132 -0
  100. synapse_sdk/utils/converters/dm/tools/bounding_box_3d.py +121 -0
  101. synapse_sdk/utils/converters/dm/tools/classification.py +75 -0
  102. synapse_sdk/utils/converters/dm/tools/keypoint.py +117 -0
  103. synapse_sdk/utils/converters/dm/tools/named_entity.py +111 -0
  104. synapse_sdk/utils/converters/dm/tools/polygon.py +122 -0
  105. synapse_sdk/utils/converters/dm/tools/polyline.py +124 -0
  106. synapse_sdk/utils/converters/dm/tools/prompt.py +94 -0
  107. synapse_sdk/utils/converters/dm/tools/relation.py +86 -0
  108. synapse_sdk/utils/converters/dm/tools/segmentation.py +141 -0
  109. synapse_sdk/utils/converters/dm/tools/segmentation_3d.py +83 -0
  110. synapse_sdk/utils/converters/dm/types.py +168 -0
  111. synapse_sdk/utils/converters/dm/utils.py +162 -0
  112. synapse_sdk/utils/converters/dm_legacy/__init__.py +56 -0
  113. synapse_sdk/utils/converters/dm_legacy/from_v1.py +627 -0
  114. synapse_sdk/utils/converters/dm_legacy/to_v1.py +367 -0
  115. synapse_sdk/utils/file/__init__.py +58 -0
  116. synapse_sdk/utils/file/archive.py +32 -0
  117. synapse_sdk/utils/file/checksum.py +56 -0
  118. synapse_sdk/utils/file/chunking.py +31 -0
  119. synapse_sdk/utils/file/download.py +385 -0
  120. synapse_sdk/utils/file/encoding.py +40 -0
  121. synapse_sdk/utils/file/io.py +22 -0
  122. synapse_sdk/utils/file/upload.py +165 -0
  123. synapse_sdk/utils/file/video/__init__.py +29 -0
  124. synapse_sdk/utils/file/video/transcode.py +307 -0
  125. synapse_sdk/utils/{file.py → file.py.backup} +77 -0
  126. synapse_sdk/utils/network.py +272 -0
  127. synapse_sdk/utils/storage/__init__.py +6 -2
  128. synapse_sdk/utils/storage/providers/file_system.py +6 -0
  129. {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/METADATA +19 -2
  130. {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/RECORD +134 -74
  131. synapse_sdk/devtools/docs/.gitignore +0 -20
  132. synapse_sdk/devtools/docs/README.md +0 -41
  133. synapse_sdk/devtools/docs/blog/2019-05-28-first-blog-post.md +0 -12
  134. synapse_sdk/devtools/docs/blog/2019-05-29-long-blog-post.md +0 -44
  135. synapse_sdk/devtools/docs/blog/2021-08-01-mdx-blog-post.mdx +0 -24
  136. synapse_sdk/devtools/docs/blog/2021-08-26-welcome/docusaurus-plushie-banner.jpeg +0 -0
  137. synapse_sdk/devtools/docs/blog/2021-08-26-welcome/index.md +0 -29
  138. synapse_sdk/devtools/docs/blog/authors.yml +0 -25
  139. synapse_sdk/devtools/docs/blog/tags.yml +0 -19
  140. synapse_sdk/devtools/docs/docusaurus.config.ts +0 -138
  141. synapse_sdk/devtools/docs/package-lock.json +0 -17455
  142. synapse_sdk/devtools/docs/package.json +0 -47
  143. synapse_sdk/devtools/docs/sidebars.ts +0 -44
  144. synapse_sdk/devtools/docs/src/components/HomepageFeatures/index.tsx +0 -71
  145. synapse_sdk/devtools/docs/src/components/HomepageFeatures/styles.module.css +0 -11
  146. synapse_sdk/devtools/docs/src/css/custom.css +0 -30
  147. synapse_sdk/devtools/docs/src/pages/index.module.css +0 -23
  148. synapse_sdk/devtools/docs/src/pages/index.tsx +0 -21
  149. synapse_sdk/devtools/docs/src/pages/markdown-page.md +0 -7
  150. synapse_sdk/devtools/docs/static/.nojekyll +0 -0
  151. synapse_sdk/devtools/docs/static/img/docusaurus-social-card.jpg +0 -0
  152. synapse_sdk/devtools/docs/static/img/docusaurus.png +0 -0
  153. synapse_sdk/devtools/docs/static/img/favicon.ico +0 -0
  154. synapse_sdk/devtools/docs/static/img/logo.png +0 -0
  155. synapse_sdk/devtools/docs/static/img/undraw_docusaurus_mountain.svg +0 -171
  156. synapse_sdk/devtools/docs/static/img/undraw_docusaurus_react.svg +0 -170
  157. synapse_sdk/devtools/docs/static/img/undraw_docusaurus_tree.svg +0 -40
  158. synapse_sdk/devtools/docs/tsconfig.json +0 -8
  159. synapse_sdk/plugins/categories/export/actions/export.py +0 -346
  160. synapse_sdk/plugins/categories/export/enums.py +0 -7
  161. synapse_sdk/plugins/categories/neural_net/actions/gradio.py +0 -151
  162. synapse_sdk/plugins/categories/pre_annotation/actions/to_task.py +0 -943
  163. synapse_sdk/plugins/categories/upload/actions/upload.py +0 -954
  164. {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/WHEEL +0 -0
  165. {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/entry_points.txt +0 -0
  166. {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/licenses/LICENSE +0 -0
  167. {synapse_sdk-1.0.0b5.dist-info → synapse_sdk-2025.12.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,214 @@
1
+ from typing import Annotated
2
+
3
+ from pydantic import AfterValidator, BaseModel, ValidationInfo, field_validator, model_validator
4
+ from pydantic_core import PydanticCustomError
5
+
6
+ from synapse_sdk.clients.exceptions import ClientError
7
+ from synapse_sdk.utils.pydantic.validators import non_blank
8
+
9
+ from .enums import VALIDATION_ERROR_MESSAGES, ValidationErrorCode
10
+
11
+
12
+ class AssetConfig(BaseModel):
13
+ """Configuration for individual asset in multi-path mode.
14
+
15
+ Used when use_single_path=False to specify unique paths and recursive
16
+ settings for each file specification.
17
+
18
+ Attributes:
19
+ path: File system path for this specific asset.
20
+ is_recursive: Whether to recursively search subdirectories for this
21
+ asset. Defaults to True.
22
+
23
+ Examples:
24
+ >>> asset_config = AssetConfig(
25
+ ... path="/sensors/camera/front",
26
+ ... is_recursive=True
27
+ ... )
28
+ """
29
+
30
+ path: str
31
+ is_recursive: bool = True
32
+
33
+
34
+ class UploadParams(BaseModel):
35
+ """Upload action parameter validation model.
36
+
37
+ Defines and validates all parameters required for upload operations.
38
+ Uses Pydantic for type validation and custom validators to ensure
39
+ storage, data_collection, and project resources exist before processing.
40
+
41
+ The model supports two operational modes controlled by the use_single_path
42
+ flag:
43
+
44
+ Single Path Mode (use_single_path=True, DEFAULT):
45
+ Traditional mode where all file specifications share one base path.
46
+ Requires 'path' parameter. Ignores 'assets' parameter.
47
+
48
+ Multi-Path Mode (use_single_path=False):
49
+ Advanced mode where each file specification has its own path.
50
+ Requires 'assets' parameter. Ignores 'path' and 'is_recursive' parameters.
51
+
52
+ Attributes:
53
+ name: Human-readable name for the upload operation. Must be non-blank.
54
+ description: Optional description of the upload operation.
55
+ use_single_path: Mode selector. True for single path mode, False for
56
+ multi-path mode. Defaults to True.
57
+ path: Base path for single path mode. Required when use_single_path=True.
58
+ is_recursive: Global recursive setting for single path mode.
59
+ Defaults to True.
60
+ assets: Per-asset path configurations for multi-path mode. Dictionary
61
+ mapping file specification names to AssetConfig objects. Required
62
+ when use_single_path=False.
63
+ storage: Storage ID where files will be uploaded. Must exist and be
64
+ accessible via client API.
65
+ data_collection: Data collection ID for organizing uploads. Must exist
66
+ and be accessible via client API.
67
+ project: Optional project ID for grouping. Must exist if specified.
68
+ excel_metadata_path: Path to Excel metadata file. Can be:
69
+ - Absolute path: '/data/metadata.xlsx'
70
+ - Relative to storage default path: 'metadata.xlsx'
71
+ - Relative to working directory (single-path mode): 'metadata.xlsx'
72
+ max_file_size_mb: Maximum file size limit in megabytes. Defaults to 50.
73
+ creating_data_unit_batch_size: Batch size for data unit creation.
74
+ Defaults to 1.
75
+ use_async_upload: Whether to use asynchronous upload processing.
76
+ Defaults to True.
77
+ extra_params: Extra parameters for the action. Optional.
78
+
79
+ Examples:
80
+ Single Path Mode (Traditional):
81
+
82
+ >>> params = UploadParams(
83
+ ... name="Standard Upload",
84
+ ... use_single_path=True,
85
+ ... path="/data/experiment_1",
86
+ ... is_recursive=True,
87
+ ... storage=1,
88
+ ... data_collection=5
89
+ ... )
90
+
91
+ Multi-Path Mode (Advanced):
92
+
93
+ >>> params = UploadParams(
94
+ ... name="Multi-Source Upload",
95
+ ... use_single_path=False,
96
+ ... assets={
97
+ ... "image_1": AssetConfig(path="/sensors/camera", is_recursive=True),
98
+ ... "pcd_1": AssetConfig(path="/sensors/lidar", is_recursive=False)
99
+ ... },
100
+ ... storage=1,
101
+ ... data_collection=5
102
+ ... )
103
+
104
+ With Excel Metadata:
105
+
106
+ >>> params = UploadParams(
107
+ ... name="Upload with Metadata",
108
+ ... path="/data/files",
109
+ ... storage=1,
110
+ ... data_collection=5,
111
+ ... excel_metadata_path="metadata.xlsx"
112
+ ... )
113
+ """
114
+
115
+ name: Annotated[str, AfterValidator(non_blank)]
116
+ description: str | None = None
117
+
118
+ # Mode selector flag (True = single path mode, False = multi-path mode)
119
+ use_single_path: bool = True
120
+
121
+ # Single path mode fields (used when use_single_path=True)
122
+ path: str | None = None
123
+ is_recursive: bool = True
124
+
125
+ # Multi-path mode fields (used when use_single_path=False)
126
+ assets: dict[str, AssetConfig] | None = None
127
+
128
+ storage: int
129
+ data_collection: int
130
+ project: int | None = None
131
+
132
+ # Excel metadata file path (absolute or relative to working directory)
133
+ excel_metadata_path: str | None = None
134
+
135
+ max_file_size_mb: int = 50
136
+ creating_data_unit_batch_size: int = 1
137
+ extra_params: dict | None = None
138
+
139
+ @field_validator('storage', mode='before')
140
+ @classmethod
141
+ def check_storage_exists(cls, value, info: ValidationInfo) -> int:
142
+ if info.context is None:
143
+ error_code = ValidationErrorCode.MISSING_CONTEXT
144
+ raise PydanticCustomError(error_code.value, VALIDATION_ERROR_MESSAGES[error_code])
145
+
146
+ action = info.context['action']
147
+ client = action.client
148
+ try:
149
+ client.get_storage(value)
150
+ except ClientError as e:
151
+ error_code = ValidationErrorCode.STORAGE_NOT_FOUND
152
+ error_message = VALIDATION_ERROR_MESSAGES[error_code].format(value, str(e))
153
+ raise PydanticCustomError(error_code.value, error_message)
154
+ return value
155
+
156
+ @field_validator('data_collection', mode='before')
157
+ @classmethod
158
+ def check_data_collection_exists(cls, value, info: ValidationInfo) -> int:
159
+ if info.context is None:
160
+ error_code = ValidationErrorCode.MISSING_CONTEXT
161
+ raise PydanticCustomError(error_code.value, VALIDATION_ERROR_MESSAGES[error_code])
162
+
163
+ action = info.context['action']
164
+ client = action.client
165
+ try:
166
+ client.get_data_collection(value)
167
+ except ClientError as e:
168
+ error_code = ValidationErrorCode.DATA_COLLECTION_NOT_FOUND
169
+ error_message = VALIDATION_ERROR_MESSAGES[error_code].format(value, str(e))
170
+ raise PydanticCustomError(error_code.value, error_message)
171
+ return value
172
+
173
+ @field_validator('project', mode='before')
174
+ @classmethod
175
+ def check_project_exists(cls, value, info: ValidationInfo) -> int | None:
176
+ if not value:
177
+ return value
178
+
179
+ if info.context is None:
180
+ error_code = ValidationErrorCode.MISSING_CONTEXT
181
+ raise PydanticCustomError(error_code.value, VALIDATION_ERROR_MESSAGES[error_code])
182
+
183
+ action = info.context['action']
184
+ client = action.client
185
+ try:
186
+ client.get_project(value)
187
+ except ClientError as e:
188
+ error_code = ValidationErrorCode.PROJECT_NOT_FOUND
189
+ error_message = VALIDATION_ERROR_MESSAGES[error_code].format(value, str(e))
190
+ raise PydanticCustomError(error_code.value, error_message)
191
+ return value
192
+
193
+ @model_validator(mode='after')
194
+ def validate_path_configuration(self) -> 'UploadParams':
195
+ """Validate path configuration based on use_single_path mode."""
196
+ if self.use_single_path:
197
+ # Single path mode: requires path
198
+ if not self.path:
199
+ raise PydanticCustomError(
200
+ 'missing_path', "When use_single_path=true (single path mode), 'path' is required"
201
+ )
202
+ # Warn if assets is provided in single path mode (it will be ignored)
203
+ # For now, we'll silently ignore it
204
+ else:
205
+ # Multi-path mode: requires assets
206
+ if not self.assets:
207
+ raise PydanticCustomError(
208
+ 'missing_assets',
209
+ "When use_single_path=false (multi-path mode), 'assets' must be provided "
210
+ 'with path configurations for each file specification',
211
+ )
212
+ # path and is_recursive are ignored in multi-path mode
213
+
214
+ return self
@@ -0,0 +1,183 @@
1
+ import traceback
2
+ from typing import Any, Dict, List
3
+
4
+ from .context import UploadContext
5
+ from .enums import LogCode
6
+ from .registry import StepRegistry
7
+ from .steps.base import BaseStep
8
+
9
+
10
+ class UploadOrchestrator:
11
+ """Facade that orchestrates the upload workflow using strategies and steps."""
12
+
13
+ def __init__(self, context: UploadContext, step_registry: StepRegistry, strategies: Dict[str, Any]):
14
+ self.context = context
15
+ self.step_registry = step_registry
16
+ self.strategies = strategies
17
+ self.executed_steps: List[BaseStep] = []
18
+ self.current_step_index = 0
19
+
20
+ def execute(self) -> Dict[str, Any]:
21
+ """Execute the complete upload workflow."""
22
+ try:
23
+ self._log_workflow_start()
24
+ self._inject_strategies_into_context()
25
+
26
+ steps = self.step_registry.get_steps()
27
+ total_steps = len(steps)
28
+
29
+ for i, step in enumerate(steps):
30
+ self.current_step_index = i
31
+
32
+ try:
33
+ result = step.safe_execute(self.context)
34
+ self.context.update(result)
35
+
36
+ if result.success:
37
+ if not result.skipped:
38
+ self.executed_steps.append(step)
39
+ self._update_progress(i + 1, total_steps)
40
+ else:
41
+ # Step failed, initiate rollback
42
+ self._log_step_failure(step, result.error)
43
+ self._rollback()
44
+ # Re-raise original exception if available, otherwise create new one
45
+ if result.original_exception:
46
+ raise result.original_exception
47
+ else:
48
+ raise Exception(f"Step '{step.name}' failed: {result.error}")
49
+
50
+ except Exception as e:
51
+ self._log_step_exception(step, str(e))
52
+ self._rollback()
53
+ raise
54
+
55
+ self._log_workflow_complete()
56
+ return self.context.get_result()
57
+
58
+ except Exception as e:
59
+ self._log_workflow_error(str(e))
60
+ # Ensure rollback is called if not already done
61
+ if not hasattr(self, '_rollback_executed'):
62
+ self._rollback()
63
+ raise
64
+
65
+ def _inject_strategies_into_context(self) -> None:
66
+ """Inject strategies into context for steps to use."""
67
+ if not hasattr(self.context, 'strategies'):
68
+ self.context.strategies = {}
69
+ self.context.strategies.update(self.strategies)
70
+
71
+ def _rollback(self) -> None:
72
+ """Rollback executed steps in reverse order."""
73
+ if hasattr(self, '_rollback_executed'):
74
+ return # Prevent multiple rollbacks
75
+
76
+ self._rollback_executed = True
77
+ self._log_rollback_start()
78
+
79
+ # Rollback in reverse order
80
+ for step in reversed(self.executed_steps):
81
+ try:
82
+ self._log_step_rollback(step)
83
+ step.rollback(self.context)
84
+ except Exception as e:
85
+ # Log rollback error but continue with other steps
86
+ self._log_rollback_error(step, str(e))
87
+
88
+ self._log_rollback_complete()
89
+
90
+ def _update_progress(self, current_step: int, total_steps: int) -> None:
91
+ """Update overall progress based on step completion."""
92
+ if total_steps == 0:
93
+ return
94
+
95
+ # Calculate progress based on step weights
96
+ completed_weight = 0.0
97
+ total_weight = self.step_registry.get_total_progress_weight()
98
+
99
+ for i, step in enumerate(self.executed_steps):
100
+ completed_weight += step.progress_weight
101
+
102
+ progress_percentage = (completed_weight / total_weight) * 100 if total_weight > 0 else 0
103
+
104
+ # Update context with progress information
105
+ self.context.update_metrics(
106
+ 'workflow',
107
+ {
108
+ 'current_step': current_step,
109
+ 'total_steps': total_steps,
110
+ 'progress_percentage': progress_percentage,
111
+ 'completed_weight': completed_weight,
112
+ 'total_weight': total_weight,
113
+ },
114
+ )
115
+
116
+ def _log_workflow_start(self) -> None:
117
+ """Log workflow start."""
118
+ steps = self.step_registry.get_steps()
119
+ step_names = [step.name for step in steps]
120
+ self.context.run.log_message_with_code(LogCode.WORKFLOW_STARTING, len(steps), step_names)
121
+
122
+ def _log_workflow_complete(self) -> None:
123
+ """Log workflow completion."""
124
+ self.context.run.log_message_with_code(LogCode.WORKFLOW_COMPLETED)
125
+
126
+ def _log_workflow_error(self, error: str) -> None:
127
+ """Log workflow error."""
128
+ self.context.run.log_message_with_code(LogCode.WORKFLOW_FAILED, error)
129
+
130
+ def _log_step_failure(self, step: BaseStep, error: str) -> None:
131
+ """Log step failure."""
132
+ self.context.run.log_message_with_code(LogCode.STEP_FAILED, step.name, error)
133
+
134
+ def _log_step_exception(self, step: BaseStep, error: str) -> None:
135
+ """Log step exception."""
136
+ self.context.run.log_message_with_code(LogCode.STEP_EXCEPTION, step.name, error)
137
+ # Log full traceback for debugging
138
+ self.context.run.log_message_with_code(LogCode.STEP_TRACEBACK, traceback.format_exc())
139
+
140
+ def _log_rollback_start(self) -> None:
141
+ """Log rollback start."""
142
+ self.context.run.log_message_with_code(LogCode.ROLLBACK_STARTING, len(self.executed_steps))
143
+
144
+ def _log_rollback_complete(self) -> None:
145
+ """Log rollback completion."""
146
+ self.context.run.log_message_with_code(LogCode.ROLLBACK_COMPLETED)
147
+
148
+ def _log_step_rollback(self, step: BaseStep) -> None:
149
+ """Log step rollback."""
150
+ self.context.run.log_message_with_code(LogCode.STEP_ROLLBACK, step.name)
151
+
152
+ def _log_rollback_error(self, step: BaseStep, error: str) -> None:
153
+ """Log rollback error."""
154
+ self.context.run.log_message_with_code(LogCode.ROLLBACK_ERROR, step.name, error)
155
+
156
+ def get_executed_steps(self) -> List[BaseStep]:
157
+ """Get list of successfully executed steps."""
158
+ return self.executed_steps.copy()
159
+
160
+ def get_current_step_index(self) -> int:
161
+ """Get current step index."""
162
+ return self.current_step_index
163
+
164
+ def get_total_steps(self) -> int:
165
+ """Get total number of steps."""
166
+ return len(self.step_registry.get_steps())
167
+
168
+ def is_rollback_executed(self) -> bool:
169
+ """Check if rollback has been executed."""
170
+ return hasattr(self, '_rollback_executed')
171
+
172
+ def get_workflow_summary(self) -> Dict[str, Any]:
173
+ """Get workflow execution summary."""
174
+ steps = self.step_registry.get_steps()
175
+ return {
176
+ 'total_steps': len(steps),
177
+ 'executed_steps': len(self.executed_steps),
178
+ 'current_step_index': self.current_step_index,
179
+ 'step_names': [step.name for step in steps],
180
+ 'executed_step_names': [step.name for step in self.executed_steps],
181
+ 'rollback_executed': self.is_rollback_executed(),
182
+ 'strategies': list(self.strategies.keys()) if self.strategies else [],
183
+ }
@@ -0,0 +1,113 @@
1
+ from typing import Dict, List, Optional
2
+
3
+ from .steps.base import BaseStep
4
+
5
+
6
+ class StepRegistry:
7
+ """Registry for managing workflow steps."""
8
+
9
+ def __init__(self):
10
+ self._steps: List[BaseStep] = []
11
+ self._step_by_name: Dict[str, BaseStep] = {}
12
+
13
+ def register(self, step: BaseStep) -> None:
14
+ """Register a step in the workflow."""
15
+ if step.name in self._step_by_name:
16
+ raise ValueError(f"Step with name '{step.name}' already registered")
17
+
18
+ self._steps.append(step)
19
+ self._step_by_name[step.name] = step
20
+
21
+ def unregister(self, step_name: str) -> bool:
22
+ """Unregister a step by name. Returns True if step was found and removed."""
23
+ if step_name not in self._step_by_name:
24
+ return False
25
+
26
+ step = self._step_by_name[step_name]
27
+ self._steps.remove(step)
28
+ del self._step_by_name[step_name]
29
+ return True
30
+
31
+ def get_steps(self) -> List[BaseStep]:
32
+ """Get all registered steps in order."""
33
+ return self._steps.copy()
34
+
35
+ def get_step(self, name: str) -> Optional[BaseStep]:
36
+ """Get a step by name."""
37
+ return self._step_by_name.get(name)
38
+
39
+ def has_step(self, name: str) -> bool:
40
+ """Check if a step is registered."""
41
+ return name in self._step_by_name
42
+
43
+ def clear(self) -> None:
44
+ """Clear all registered steps."""
45
+ self._steps.clear()
46
+ self._step_by_name.clear()
47
+
48
+ def get_step_names(self) -> List[str]:
49
+ """Get list of all registered step names."""
50
+ return list(self._step_by_name.keys())
51
+
52
+ def get_total_progress_weight(self) -> float:
53
+ """Get total progress weight of all steps."""
54
+ return sum(step.progress_weight for step in self._steps)
55
+
56
+ def insert_step_after(self, after_step_name: str, new_step: BaseStep) -> None:
57
+ """Insert a step after an existing step."""
58
+ if after_step_name not in self._step_by_name:
59
+ raise ValueError(f"Step '{after_step_name}' not found")
60
+
61
+ if new_step.name in self._step_by_name:
62
+ raise ValueError(f"Step with name '{new_step.name}' already registered")
63
+
64
+ # Find the index of the step to insert after
65
+ after_step = self._step_by_name[after_step_name]
66
+ index = self._steps.index(after_step) + 1
67
+
68
+ # Insert the new step
69
+ self._steps.insert(index, new_step)
70
+ self._step_by_name[new_step.name] = new_step
71
+
72
+ def insert_step_before(self, before_step_name: str, new_step: BaseStep) -> None:
73
+ """Insert a step before an existing step."""
74
+ if before_step_name not in self._step_by_name:
75
+ raise ValueError(f"Step '{before_step_name}' not found")
76
+
77
+ if new_step.name in self._step_by_name:
78
+ raise ValueError(f"Step with name '{new_step.name}' already registered")
79
+
80
+ # Find the index of the step to insert before
81
+ before_step = self._step_by_name[before_step_name]
82
+ index = self._steps.index(before_step)
83
+
84
+ # Insert the new step
85
+ self._steps.insert(index, new_step)
86
+ self._step_by_name[new_step.name] = new_step
87
+
88
+ def reorder_steps(self, step_names: List[str]) -> None:
89
+ """Reorder steps according to the provided list of step names."""
90
+ if set(step_names) != set(self._step_by_name.keys()):
91
+ raise ValueError('Step names list must contain all registered steps')
92
+
93
+ # Reorder the steps list
94
+ self._steps = [self._step_by_name[name] for name in step_names]
95
+
96
+ def __len__(self) -> int:
97
+ """Return number of registered steps."""
98
+ return len(self._steps)
99
+
100
+ def __iter__(self):
101
+ """Iterate over registered steps."""
102
+ return iter(self._steps)
103
+
104
+ def __contains__(self, step_name: str) -> bool:
105
+ """Check if step name is registered."""
106
+ return step_name in self._step_by_name
107
+
108
+ def __str__(self) -> str:
109
+ step_names = [step.name for step in self._steps]
110
+ return f'StepRegistry({step_names})'
111
+
112
+ def __repr__(self) -> str:
113
+ return f'StepRegistry(steps={len(self._steps)})'
@@ -0,0 +1,179 @@
1
+ import json
2
+ from datetime import datetime
3
+ from typing import Optional
4
+
5
+ from pydantic import BaseModel
6
+
7
+ from synapse_sdk.plugins.models import Run
8
+ from synapse_sdk.shared.enums import Context
9
+
10
+ from .enums import LOG_MESSAGES, LogCode, UploadStatus
11
+ from .utils import PathAwareJSONEncoder
12
+
13
+
14
+ class UploadRun(Run):
15
+ """Upload-specific run management class.
16
+
17
+ Extends the base Run class with upload-specific logging capabilities
18
+ and event tracking. Provides type-safe logging using LogCode enums
19
+ and specialized methods for tracking upload progress.
20
+
21
+ Manages logging for upload events, data files, data units, and tasks
22
+ throughout the upload lifecycle. Each log entry includes status,
23
+ timestamps, and relevant metadata.
24
+
25
+ Attributes:
26
+ Inherits all attributes from base Run class plus upload-specific
27
+ logging methods and nested model classes for structured logging.
28
+
29
+ Example:
30
+ >>> run = UploadRun(job_id, context)
31
+ >>> run.log_message_with_code(LogCode.UPLOAD_STARTED)
32
+ >>> run.log_upload_event(LogCode.FILES_DISCOVERED, file_count)
33
+ """
34
+
35
+ class UploadEventLog(BaseModel):
36
+ """Model for upload event log entries.
37
+
38
+ Records significant events during upload processing with
39
+ status information and timestamps.
40
+
41
+ Attributes:
42
+ info (str | None): Optional additional information
43
+ status (Context): Event status/severity level
44
+ created (str): Timestamp when event occurred
45
+ """
46
+
47
+ info: Optional[str] = None
48
+ status: Context
49
+ created: str
50
+
51
+ class DataFileLog(BaseModel):
52
+ """Model for data file processing log entries.
53
+
54
+ Tracks the processing status of individual data files
55
+ during upload operations.
56
+
57
+ Attributes:
58
+ data_file_info (str | None): Information about the data file
59
+ status (UploadStatus): Processing status (SUCCESS/FAILED)
60
+ created (str): Timestamp when log entry was created
61
+ """
62
+
63
+ data_file_info: str | None
64
+ status: UploadStatus
65
+ created: str
66
+
67
+ class DataUnitLog(BaseModel):
68
+ """Model for data unit creation log entries.
69
+
70
+ Records the creation status of data units generated from
71
+ uploaded files, including metadata and identifiers.
72
+
73
+ Attributes:
74
+ data_unit_id (int | None): ID of created data unit
75
+ status (UploadStatus): Creation status (SUCCESS/FAILED)
76
+ created (str): Timestamp when log entry was created
77
+ data_unit_meta (dict | None): Metadata associated with data unit
78
+ """
79
+
80
+ data_unit_id: int | None
81
+ status: UploadStatus
82
+ created: str
83
+ data_unit_meta: dict | None
84
+
85
+ class TaskLog(BaseModel):
86
+ """Model for task execution log entries.
87
+
88
+ Tracks the execution status of background tasks related
89
+ to upload processing.
90
+
91
+ Attributes:
92
+ task_id (int | None): ID of the executed task
93
+ status (UploadStatus): Task execution status (SUCCESS/FAILED)
94
+ created (str): Timestamp when log entry was created
95
+ """
96
+
97
+ task_id: int | None
98
+ status: UploadStatus
99
+ created: str
100
+
101
+ class MetricsRecord(BaseModel):
102
+ """Model for upload metrics tracking.
103
+
104
+ Records count-based metrics for monitoring upload
105
+ progress and success rates.
106
+
107
+ Attributes:
108
+ stand_by (int): Number of items waiting to be processed
109
+ failed (int): Number of items that failed processing
110
+ success (int): Number of items successfully processed
111
+ """
112
+
113
+ stand_by: int
114
+ failed: int
115
+ success: int
116
+
117
+ def log_message_with_code(self, code: LogCode, *args, level: Optional[Context] = None):
118
+ if code not in LOG_MESSAGES:
119
+ # Use direct log_message to avoid recursion
120
+ self.log_message(f'Unknown log code: {code}')
121
+ return
122
+
123
+ log_config = LOG_MESSAGES[code]
124
+ message = log_config['message'].format(*args) if args else log_config['message']
125
+ log_level = level or log_config['level'] or Context.INFO
126
+
127
+ # Always call log_message for basic logging
128
+ if log_level:
129
+ self.log_message(message, context=log_level.value)
130
+ else:
131
+ self.log_message(message)
132
+
133
+ def log_upload_event(self, code: LogCode, *args, level: Optional[Context] = None):
134
+ # Call log_message_with_code to handle the basic logging
135
+ self.log_message_with_code(code, *args, level=level)
136
+
137
+ # Also log the event for upload-specific tracking
138
+ if code not in LOG_MESSAGES:
139
+ now = datetime.now().isoformat()
140
+ self.log(
141
+ 'upload_event',
142
+ self.UploadEventLog(info=f'Unknown log code: {code}', status=Context.DANGER, created=now).model_dump(),
143
+ )
144
+ return
145
+
146
+ log_config = LOG_MESSAGES[code]
147
+ message = log_config['message'].format(*args) if args else log_config['message']
148
+ log_level = level or log_config['level'] or Context.INFO
149
+
150
+ now = datetime.now().isoformat()
151
+ self.log(
152
+ 'upload_event',
153
+ self.UploadEventLog(info=message, status=log_level, created=now).model_dump(),
154
+ )
155
+
156
+ def log_data_file(self, data_file_info: dict, status: UploadStatus):
157
+ now = datetime.now().isoformat()
158
+ data_file_info_str = json.dumps(data_file_info, ensure_ascii=False, cls=PathAwareJSONEncoder)
159
+ self.log(
160
+ 'upload_data_file',
161
+ self.DataFileLog(data_file_info=data_file_info_str, status=status, created=now).model_dump(),
162
+ )
163
+
164
+ def log_data_unit(self, data_unit_id: int, status: UploadStatus, data_unit_meta: dict | None = None):
165
+ now = datetime.now().isoformat()
166
+ self.log(
167
+ 'upload_data_unit',
168
+ self.DataUnitLog(
169
+ data_unit_id=data_unit_id, status=status, created=now, data_unit_meta=data_unit_meta
170
+ ).model_dump(),
171
+ )
172
+
173
+ def log_task(self, task_id: int, status: UploadStatus):
174
+ now = datetime.now().isoformat()
175
+ self.log('upload_task', self.TaskLog(task_id=task_id, status=status, created=now).model_dump())
176
+
177
+ def log_metrics(self, record: MetricsRecord, category: str):
178
+ record = self.MetricsRecord.model_validate(record)
179
+ self.set_metrics(value=record.model_dump(), category=category)
@@ -0,0 +1 @@
1
+ # Step-based workflow implementations for upload actions