synapse-sdk 1.0.0a11__py3-none-any.whl → 2026.1.1b2__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.
Potentially problematic release.
This version of synapse-sdk might be problematic. Click here for more details.
- synapse_sdk/__init__.py +24 -0
- synapse_sdk/cli/__init__.py +9 -8
- synapse_sdk/cli/agent/__init__.py +25 -0
- synapse_sdk/cli/agent/config.py +104 -0
- synapse_sdk/cli/agent/select.py +197 -0
- synapse_sdk/cli/auth.py +104 -0
- synapse_sdk/cli/main.py +1025 -0
- synapse_sdk/cli/plugin/__init__.py +58 -0
- synapse_sdk/cli/plugin/create.py +566 -0
- synapse_sdk/cli/plugin/job.py +196 -0
- synapse_sdk/cli/plugin/publish.py +322 -0
- synapse_sdk/cli/plugin/run.py +131 -0
- synapse_sdk/cli/plugin/test.py +200 -0
- synapse_sdk/clients/README.md +239 -0
- synapse_sdk/clients/__init__.py +5 -0
- synapse_sdk/clients/_template.py +266 -0
- synapse_sdk/clients/agent/__init__.py +84 -29
- synapse_sdk/clients/agent/async_ray.py +289 -0
- synapse_sdk/clients/agent/container.py +83 -0
- synapse_sdk/clients/agent/plugin.py +101 -0
- synapse_sdk/clients/agent/ray.py +296 -39
- synapse_sdk/clients/backend/__init__.py +152 -12
- synapse_sdk/clients/backend/annotation.py +164 -22
- synapse_sdk/clients/backend/core.py +101 -0
- synapse_sdk/clients/backend/data_collection.py +292 -0
- synapse_sdk/clients/backend/hitl.py +87 -0
- synapse_sdk/clients/backend/integration.py +374 -46
- synapse_sdk/clients/backend/ml.py +134 -22
- synapse_sdk/clients/backend/models.py +247 -0
- synapse_sdk/clients/base.py +538 -59
- synapse_sdk/clients/exceptions.py +35 -7
- synapse_sdk/clients/pipeline/__init__.py +5 -0
- synapse_sdk/clients/pipeline/client.py +636 -0
- synapse_sdk/clients/protocols.py +178 -0
- synapse_sdk/clients/utils.py +86 -8
- synapse_sdk/clients/validation.py +58 -0
- synapse_sdk/enums.py +76 -0
- synapse_sdk/exceptions.py +168 -0
- synapse_sdk/integrations/__init__.py +74 -0
- synapse_sdk/integrations/_base.py +119 -0
- synapse_sdk/integrations/_context.py +53 -0
- synapse_sdk/integrations/ultralytics/__init__.py +78 -0
- synapse_sdk/integrations/ultralytics/_callbacks.py +126 -0
- synapse_sdk/integrations/ultralytics/_patches.py +124 -0
- synapse_sdk/loggers.py +476 -95
- synapse_sdk/mcp/MCP.md +69 -0
- synapse_sdk/mcp/__init__.py +48 -0
- synapse_sdk/mcp/__main__.py +6 -0
- synapse_sdk/mcp/config.py +349 -0
- synapse_sdk/mcp/prompts/__init__.py +4 -0
- synapse_sdk/mcp/resources/__init__.py +4 -0
- synapse_sdk/mcp/server.py +1352 -0
- synapse_sdk/mcp/tools/__init__.py +6 -0
- synapse_sdk/plugins/__init__.py +133 -9
- synapse_sdk/plugins/action.py +229 -0
- synapse_sdk/plugins/actions/__init__.py +82 -0
- synapse_sdk/plugins/actions/dataset/__init__.py +37 -0
- synapse_sdk/plugins/actions/dataset/action.py +471 -0
- synapse_sdk/plugins/actions/export/__init__.py +55 -0
- synapse_sdk/plugins/actions/export/action.py +183 -0
- synapse_sdk/plugins/actions/export/context.py +59 -0
- synapse_sdk/plugins/actions/inference/__init__.py +84 -0
- synapse_sdk/plugins/actions/inference/action.py +285 -0
- synapse_sdk/plugins/actions/inference/context.py +81 -0
- synapse_sdk/plugins/actions/inference/deployment.py +322 -0
- synapse_sdk/plugins/actions/inference/serve.py +252 -0
- synapse_sdk/plugins/actions/train/__init__.py +54 -0
- synapse_sdk/plugins/actions/train/action.py +326 -0
- synapse_sdk/plugins/actions/train/context.py +57 -0
- synapse_sdk/plugins/actions/upload/__init__.py +49 -0
- synapse_sdk/plugins/actions/upload/action.py +165 -0
- synapse_sdk/plugins/actions/upload/context.py +61 -0
- synapse_sdk/plugins/config.py +98 -0
- synapse_sdk/plugins/context/__init__.py +109 -0
- synapse_sdk/plugins/context/env.py +113 -0
- synapse_sdk/plugins/datasets/__init__.py +113 -0
- synapse_sdk/plugins/datasets/converters/__init__.py +76 -0
- synapse_sdk/plugins/datasets/converters/base.py +347 -0
- synapse_sdk/plugins/datasets/converters/yolo/__init__.py +9 -0
- synapse_sdk/plugins/datasets/converters/yolo/from_dm.py +468 -0
- synapse_sdk/plugins/datasets/converters/yolo/to_dm.py +381 -0
- synapse_sdk/plugins/datasets/formats/__init__.py +82 -0
- synapse_sdk/plugins/datasets/formats/dm.py +351 -0
- synapse_sdk/plugins/datasets/formats/yolo.py +240 -0
- synapse_sdk/plugins/decorators.py +83 -0
- synapse_sdk/plugins/discovery.py +790 -0
- synapse_sdk/plugins/docs/ACTION_DEV_GUIDE.md +933 -0
- synapse_sdk/plugins/docs/ARCHITECTURE.md +1225 -0
- synapse_sdk/plugins/docs/LOGGING_SYSTEM.md +683 -0
- synapse_sdk/plugins/docs/OVERVIEW.md +531 -0
- synapse_sdk/plugins/docs/PIPELINE_GUIDE.md +145 -0
- synapse_sdk/plugins/docs/README.md +513 -0
- synapse_sdk/plugins/docs/STEP.md +656 -0
- synapse_sdk/plugins/enums.py +70 -10
- synapse_sdk/plugins/errors.py +92 -0
- synapse_sdk/plugins/executors/__init__.py +43 -0
- synapse_sdk/plugins/executors/local.py +99 -0
- synapse_sdk/plugins/executors/ray/__init__.py +18 -0
- synapse_sdk/plugins/executors/ray/base.py +282 -0
- synapse_sdk/plugins/executors/ray/job.py +298 -0
- synapse_sdk/plugins/executors/ray/jobs_api.py +511 -0
- synapse_sdk/plugins/executors/ray/packaging.py +137 -0
- synapse_sdk/plugins/executors/ray/pipeline.py +792 -0
- synapse_sdk/plugins/executors/ray/task.py +257 -0
- synapse_sdk/plugins/models/__init__.py +26 -0
- synapse_sdk/plugins/models/logger.py +173 -0
- synapse_sdk/plugins/models/pipeline.py +25 -0
- synapse_sdk/plugins/pipelines/__init__.py +81 -0
- synapse_sdk/plugins/pipelines/action_pipeline.py +417 -0
- synapse_sdk/plugins/pipelines/context.py +107 -0
- synapse_sdk/plugins/pipelines/display.py +311 -0
- synapse_sdk/plugins/runner.py +114 -0
- synapse_sdk/plugins/schemas/__init__.py +19 -0
- synapse_sdk/plugins/schemas/results.py +152 -0
- synapse_sdk/plugins/steps/__init__.py +63 -0
- synapse_sdk/plugins/steps/base.py +128 -0
- synapse_sdk/plugins/steps/context.py +90 -0
- synapse_sdk/plugins/steps/orchestrator.py +128 -0
- synapse_sdk/plugins/steps/registry.py +103 -0
- synapse_sdk/plugins/steps/utils/__init__.py +20 -0
- synapse_sdk/plugins/steps/utils/logging.py +85 -0
- synapse_sdk/plugins/steps/utils/timing.py +71 -0
- synapse_sdk/plugins/steps/utils/validation.py +68 -0
- synapse_sdk/plugins/templates/__init__.py +50 -0
- synapse_sdk/plugins/templates/base/.gitignore.j2 +26 -0
- synapse_sdk/plugins/templates/base/.synapseignore.j2 +11 -0
- synapse_sdk/plugins/templates/base/README.md.j2 +26 -0
- synapse_sdk/plugins/templates/base/plugin/__init__.py.j2 +1 -0
- synapse_sdk/plugins/templates/base/pyproject.toml.j2 +14 -0
- synapse_sdk/plugins/templates/base/requirements.txt.j2 +1 -0
- synapse_sdk/plugins/templates/custom/plugin/main.py.j2 +18 -0
- synapse_sdk/plugins/templates/data_validation/plugin/validate.py.j2 +32 -0
- synapse_sdk/plugins/templates/export/plugin/export.py.j2 +36 -0
- synapse_sdk/plugins/templates/neural_net/plugin/inference.py.j2 +36 -0
- synapse_sdk/plugins/templates/neural_net/plugin/train.py.j2 +33 -0
- synapse_sdk/plugins/templates/post_annotation/plugin/post_annotate.py.j2 +32 -0
- synapse_sdk/plugins/templates/pre_annotation/plugin/pre_annotate.py.j2 +32 -0
- synapse_sdk/plugins/templates/smart_tool/plugin/auto_label.py.j2 +44 -0
- synapse_sdk/plugins/templates/upload/plugin/upload.py.j2 +35 -0
- synapse_sdk/plugins/testing/__init__.py +25 -0
- synapse_sdk/plugins/testing/sample_actions.py +98 -0
- synapse_sdk/plugins/types.py +206 -0
- synapse_sdk/plugins/upload.py +595 -64
- synapse_sdk/plugins/utils.py +325 -37
- synapse_sdk/shared/__init__.py +25 -0
- synapse_sdk/utils/__init__.py +1 -0
- synapse_sdk/utils/auth.py +74 -0
- synapse_sdk/utils/file/__init__.py +58 -0
- synapse_sdk/utils/file/archive.py +449 -0
- synapse_sdk/utils/file/checksum.py +167 -0
- synapse_sdk/utils/file/download.py +286 -0
- synapse_sdk/utils/file/io.py +129 -0
- synapse_sdk/utils/file/requirements.py +36 -0
- synapse_sdk/utils/network.py +168 -0
- synapse_sdk/utils/storage/__init__.py +238 -0
- synapse_sdk/utils/storage/config.py +188 -0
- synapse_sdk/utils/storage/errors.py +52 -0
- synapse_sdk/utils/storage/providers/__init__.py +13 -0
- synapse_sdk/utils/storage/providers/base.py +76 -0
- synapse_sdk/utils/storage/providers/gcs.py +168 -0
- synapse_sdk/utils/storage/providers/http.py +250 -0
- synapse_sdk/utils/storage/providers/local.py +126 -0
- synapse_sdk/utils/storage/providers/s3.py +177 -0
- synapse_sdk/utils/storage/providers/sftp.py +208 -0
- synapse_sdk/utils/storage/registry.py +125 -0
- synapse_sdk/utils/websocket.py +99 -0
- synapse_sdk-2026.1.1b2.dist-info/METADATA +715 -0
- synapse_sdk-2026.1.1b2.dist-info/RECORD +172 -0
- {synapse_sdk-1.0.0a11.dist-info → synapse_sdk-2026.1.1b2.dist-info}/WHEEL +1 -1
- synapse_sdk-2026.1.1b2.dist-info/licenses/LICENSE +201 -0
- locale/en/LC_MESSAGES/messages.mo +0 -0
- locale/en/LC_MESSAGES/messages.po +0 -39
- locale/ko/LC_MESSAGES/messages.mo +0 -0
- locale/ko/LC_MESSAGES/messages.po +0 -34
- synapse_sdk/cli/create_plugin.py +0 -10
- synapse_sdk/clients/agent/core.py +0 -7
- synapse_sdk/clients/agent/service.py +0 -15
- synapse_sdk/clients/backend/dataset.py +0 -51
- synapse_sdk/clients/ray/__init__.py +0 -6
- synapse_sdk/clients/ray/core.py +0 -22
- synapse_sdk/clients/ray/serve.py +0 -20
- synapse_sdk/i18n.py +0 -35
- synapse_sdk/plugins/categories/__init__.py +0 -0
- synapse_sdk/plugins/categories/base.py +0 -235
- synapse_sdk/plugins/categories/data_validation/__init__.py +0 -0
- synapse_sdk/plugins/categories/data_validation/actions/__init__.py +0 -0
- synapse_sdk/plugins/categories/data_validation/actions/validation.py +0 -10
- synapse_sdk/plugins/categories/data_validation/templates/config.yaml +0 -3
- synapse_sdk/plugins/categories/data_validation/templates/plugin/__init__.py +0 -0
- synapse_sdk/plugins/categories/data_validation/templates/plugin/validation.py +0 -5
- synapse_sdk/plugins/categories/decorators.py +0 -13
- synapse_sdk/plugins/categories/export/__init__.py +0 -0
- synapse_sdk/plugins/categories/export/actions/__init__.py +0 -0
- synapse_sdk/plugins/categories/export/actions/export.py +0 -10
- synapse_sdk/plugins/categories/import/__init__.py +0 -0
- synapse_sdk/plugins/categories/import/actions/__init__.py +0 -0
- synapse_sdk/plugins/categories/import/actions/import.py +0 -10
- synapse_sdk/plugins/categories/neural_net/__init__.py +0 -0
- synapse_sdk/plugins/categories/neural_net/actions/__init__.py +0 -0
- synapse_sdk/plugins/categories/neural_net/actions/deployment.py +0 -45
- synapse_sdk/plugins/categories/neural_net/actions/inference.py +0 -18
- synapse_sdk/plugins/categories/neural_net/actions/test.py +0 -10
- synapse_sdk/plugins/categories/neural_net/actions/train.py +0 -143
- synapse_sdk/plugins/categories/neural_net/templates/config.yaml +0 -12
- synapse_sdk/plugins/categories/neural_net/templates/plugin/__init__.py +0 -0
- synapse_sdk/plugins/categories/neural_net/templates/plugin/inference.py +0 -4
- synapse_sdk/plugins/categories/neural_net/templates/plugin/test.py +0 -2
- synapse_sdk/plugins/categories/neural_net/templates/plugin/train.py +0 -14
- synapse_sdk/plugins/categories/post_annotation/__init__.py +0 -0
- synapse_sdk/plugins/categories/post_annotation/actions/__init__.py +0 -0
- synapse_sdk/plugins/categories/post_annotation/actions/post_annotation.py +0 -10
- synapse_sdk/plugins/categories/post_annotation/templates/config.yaml +0 -3
- synapse_sdk/plugins/categories/post_annotation/templates/plugin/__init__.py +0 -0
- synapse_sdk/plugins/categories/post_annotation/templates/plugin/post_annotation.py +0 -3
- synapse_sdk/plugins/categories/pre_annotation/__init__.py +0 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/__init__.py +0 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation.py +0 -10
- synapse_sdk/plugins/categories/pre_annotation/templates/config.yaml +0 -3
- synapse_sdk/plugins/categories/pre_annotation/templates/plugin/__init__.py +0 -0
- synapse_sdk/plugins/categories/pre_annotation/templates/plugin/pre_annotation.py +0 -3
- synapse_sdk/plugins/categories/registry.py +0 -16
- synapse_sdk/plugins/categories/smart_tool/__init__.py +0 -0
- synapse_sdk/plugins/categories/smart_tool/actions/__init__.py +0 -0
- synapse_sdk/plugins/categories/smart_tool/actions/auto_label.py +0 -37
- synapse_sdk/plugins/categories/smart_tool/templates/config.yaml +0 -7
- synapse_sdk/plugins/categories/smart_tool/templates/plugin/__init__.py +0 -0
- synapse_sdk/plugins/categories/smart_tool/templates/plugin/auto_label.py +0 -11
- synapse_sdk/plugins/categories/templates.py +0 -32
- synapse_sdk/plugins/cli/__init__.py +0 -21
- synapse_sdk/plugins/cli/publish.py +0 -37
- synapse_sdk/plugins/cli/run.py +0 -67
- synapse_sdk/plugins/exceptions.py +0 -22
- synapse_sdk/plugins/models.py +0 -121
- synapse_sdk/plugins/templates/cookiecutter.json +0 -11
- synapse_sdk/plugins/templates/hooks/post_gen_project.py +0 -3
- synapse_sdk/plugins/templates/hooks/pre_prompt.py +0 -21
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.env +0 -24
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.env.dist +0 -24
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.gitignore +0 -27
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.pre-commit-config.yaml +0 -7
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/README.md +0 -5
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/config.yaml +0 -6
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/main.py +0 -4
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/plugin/__init__.py +0 -0
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/pyproject.toml +0 -13
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/requirements.txt +0 -1
- synapse_sdk/shared/enums.py +0 -8
- synapse_sdk/utils/debug.py +0 -5
- synapse_sdk/utils/file.py +0 -87
- synapse_sdk/utils/module_loading.py +0 -29
- synapse_sdk/utils/pydantic/__init__.py +0 -0
- synapse_sdk/utils/pydantic/config.py +0 -4
- synapse_sdk/utils/pydantic/errors.py +0 -33
- synapse_sdk/utils/pydantic/validators.py +0 -7
- synapse_sdk/utils/storage.py +0 -91
- synapse_sdk/utils/string.py +0 -11
- synapse_sdk-1.0.0a11.dist-info/LICENSE +0 -21
- synapse_sdk-1.0.0a11.dist-info/METADATA +0 -43
- synapse_sdk-1.0.0a11.dist-info/RECORD +0 -111
- {synapse_sdk-1.0.0a11.dist-info → synapse_sdk-2026.1.1b2.dist-info}/entry_points.txt +0 -0
- {synapse_sdk-1.0.0a11.dist-info → synapse_sdk-2026.1.1b2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""S3-compatible storage provider (AWS S3, MinIO)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from synapse_sdk.utils.storage.config import S3StorageConfig
|
|
9
|
+
from synapse_sdk.utils.storage.errors import (
|
|
10
|
+
StorageConnectionError,
|
|
11
|
+
StorageNotFoundError,
|
|
12
|
+
StorageUploadError,
|
|
13
|
+
)
|
|
14
|
+
from synapse_sdk.utils.storage.providers.base import _BaseStorageMixin
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from upath import UPath
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class S3Storage(_BaseStorageMixin):
|
|
21
|
+
"""Storage provider for S3-compatible services (AWS S3, MinIO).
|
|
22
|
+
|
|
23
|
+
Requires: universal-pathlib[s3] (pip install universal-pathlib[s3])
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
config: Configuration dict with S3 credentials.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
>>> storage = S3Storage({
|
|
30
|
+
... 'bucket_name': 'my-bucket',
|
|
31
|
+
... 'access_key': 'AKIAIOSFODNN7EXAMPLE',
|
|
32
|
+
... 'secret_key': 'wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY',
|
|
33
|
+
... 'region_name': 'us-east-1',
|
|
34
|
+
... })
|
|
35
|
+
>>> storage.upload(Path('/tmp/file.txt'), 'data/file.txt')
|
|
36
|
+
's3://my-bucket/data/file.txt'
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
DEFAULT_REGION = 'us-east-1'
|
|
40
|
+
|
|
41
|
+
def __init__(self, config: dict[str, Any]):
|
|
42
|
+
try:
|
|
43
|
+
from upath import UPath
|
|
44
|
+
except ImportError as e:
|
|
45
|
+
raise ImportError(
|
|
46
|
+
'S3Storage requires universal-pathlib[s3]. Install with: pip install universal-pathlib[s3]'
|
|
47
|
+
) from e
|
|
48
|
+
|
|
49
|
+
validated = S3StorageConfig.model_validate(config)
|
|
50
|
+
|
|
51
|
+
self._bucket_name = validated.bucket_name
|
|
52
|
+
client_kwargs: dict[str, Any] = {
|
|
53
|
+
'region_name': validated.region_name or self.DEFAULT_REGION,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if validated.endpoint_url:
|
|
57
|
+
client_kwargs['endpoint_url'] = validated.endpoint_url
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
self._upath = UPath(
|
|
61
|
+
f's3://{validated.bucket_name}',
|
|
62
|
+
key=validated.access_key,
|
|
63
|
+
secret=validated.secret_key,
|
|
64
|
+
client_kwargs=client_kwargs,
|
|
65
|
+
)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
raise StorageConnectionError(
|
|
68
|
+
f'Failed to connect to S3: {e}',
|
|
69
|
+
details={'bucket': validated.bucket_name},
|
|
70
|
+
) from e
|
|
71
|
+
|
|
72
|
+
def upload(self, source: Path, target: str) -> str:
|
|
73
|
+
"""Upload a file to S3.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
source: Local file path to upload.
|
|
77
|
+
target: Target path in S3 bucket.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
s3:// URL of uploaded file.
|
|
81
|
+
"""
|
|
82
|
+
source_path = Path(source) if isinstance(source, str) else source
|
|
83
|
+
|
|
84
|
+
if not source_path.exists():
|
|
85
|
+
raise StorageNotFoundError(
|
|
86
|
+
f'Source file not found: {source_path}',
|
|
87
|
+
details={'source': str(source_path)},
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
target_path = self._normalize_path(target)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
with open(source_path, 'rb') as f:
|
|
94
|
+
(self._upath / target_path).write_bytes(f.read())
|
|
95
|
+
except Exception as e:
|
|
96
|
+
raise StorageUploadError(
|
|
97
|
+
f'Failed to upload to S3: {e}',
|
|
98
|
+
details={'source': str(source_path), 'target': target_path},
|
|
99
|
+
) from e
|
|
100
|
+
|
|
101
|
+
return self.get_url(target)
|
|
102
|
+
|
|
103
|
+
def exists(self, target: str) -> bool:
|
|
104
|
+
"""Check if file exists in S3.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
target: Path to check.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
True if exists, False otherwise.
|
|
111
|
+
"""
|
|
112
|
+
target_path = self._normalize_path(target)
|
|
113
|
+
return (self._upath / target_path).exists()
|
|
114
|
+
|
|
115
|
+
def get_url(self, target: str) -> str:
|
|
116
|
+
"""Get s3:// URL for target.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
target: Target path.
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
s3:// URL string.
|
|
123
|
+
"""
|
|
124
|
+
target_path = self._normalize_path(target)
|
|
125
|
+
if target_path:
|
|
126
|
+
return f's3://{self._bucket_name}/{target_path}'
|
|
127
|
+
return f's3://{self._bucket_name}'
|
|
128
|
+
|
|
129
|
+
def get_pathlib(self, path: str) -> UPath:
|
|
130
|
+
"""Get UPath object for path.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
path: Path relative to bucket root.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
UPath object.
|
|
137
|
+
"""
|
|
138
|
+
normalized = self._normalize_path(path)
|
|
139
|
+
if not normalized:
|
|
140
|
+
return self._upath
|
|
141
|
+
return self._upath / normalized
|
|
142
|
+
|
|
143
|
+
def get_path_file_count(self, pathlib_obj: UPath) -> int:
|
|
144
|
+
"""Count files in S3 path.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
pathlib_obj: UPath object from get_pathlib().
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Number of files.
|
|
151
|
+
"""
|
|
152
|
+
return self._count_files(pathlib_obj)
|
|
153
|
+
|
|
154
|
+
def get_path_total_size(self, pathlib_obj: UPath) -> int:
|
|
155
|
+
"""Calculate total size of files in S3 path.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
pathlib_obj: UPath object from get_pathlib().
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Total size in bytes.
|
|
162
|
+
"""
|
|
163
|
+
return self._calculate_total_size(pathlib_obj)
|
|
164
|
+
|
|
165
|
+
def glob(self, pattern: str) -> list[UPath]:
|
|
166
|
+
"""Glob pattern matching in S3.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
pattern: Glob pattern.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
List of matching UPath objects.
|
|
173
|
+
"""
|
|
174
|
+
return list(self._upath.glob(pattern))
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
__all__ = ['S3Storage']
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""SFTP storage provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, Any
|
|
7
|
+
|
|
8
|
+
from synapse_sdk.utils.storage.config import SFTPStorageConfig
|
|
9
|
+
from synapse_sdk.utils.storage.errors import (
|
|
10
|
+
StorageConnectionError,
|
|
11
|
+
StorageNotFoundError,
|
|
12
|
+
StorageUploadError,
|
|
13
|
+
)
|
|
14
|
+
from synapse_sdk.utils.storage.providers.base import _BaseStorageMixin
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from upath import UPath
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SFTPStorage(_BaseStorageMixin):
|
|
21
|
+
"""Storage provider for SFTP servers.
|
|
22
|
+
|
|
23
|
+
Requires: universal-pathlib[sftp] (pip install universal-pathlib[sftp])
|
|
24
|
+
|
|
25
|
+
Supports both password and private key authentication.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
config: Configuration dict with SFTP credentials.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
>>> # Password authentication
|
|
32
|
+
>>> storage = SFTPStorage({
|
|
33
|
+
... 'host': 'sftp.example.com',
|
|
34
|
+
... 'username': 'user',
|
|
35
|
+
... 'password': 'secret',
|
|
36
|
+
... 'root_path': '/data',
|
|
37
|
+
... })
|
|
38
|
+
>>>
|
|
39
|
+
>>> # Private key authentication
|
|
40
|
+
>>> storage = SFTPStorage({
|
|
41
|
+
... 'host': 'sftp.example.com',
|
|
42
|
+
... 'username': 'user',
|
|
43
|
+
... 'private_key': '/path/to/id_rsa',
|
|
44
|
+
... 'root_path': '/data',
|
|
45
|
+
... })
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(self, config: dict[str, Any]):
|
|
49
|
+
try:
|
|
50
|
+
from upath import UPath
|
|
51
|
+
except ImportError as e:
|
|
52
|
+
raise ImportError(
|
|
53
|
+
'SFTPStorage requires universal-pathlib[sftp]. Install with: pip install universal-pathlib[sftp]'
|
|
54
|
+
) from e
|
|
55
|
+
|
|
56
|
+
validated = SFTPStorageConfig.model_validate(config)
|
|
57
|
+
self._config = validated
|
|
58
|
+
|
|
59
|
+
# Build UPath kwargs based on auth method
|
|
60
|
+
upath_kwargs: dict[str, Any] = {
|
|
61
|
+
'username': validated.username,
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if validated.password:
|
|
65
|
+
upath_kwargs['password'] = validated.password
|
|
66
|
+
|
|
67
|
+
if validated.private_key:
|
|
68
|
+
upath_kwargs['key_filename'] = validated.private_key
|
|
69
|
+
if validated.private_key_passphrase:
|
|
70
|
+
upath_kwargs['passphrase'] = validated.private_key_passphrase
|
|
71
|
+
|
|
72
|
+
# Construct base URL with port if non-default
|
|
73
|
+
if validated.port != 22:
|
|
74
|
+
base_url = f'sftp://{validated.host}:{validated.port}'
|
|
75
|
+
else:
|
|
76
|
+
base_url = f'sftp://{validated.host}'
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
self._base_upath = UPath(base_url, **upath_kwargs)
|
|
80
|
+
self._root_path = validated.root_path.rstrip('/')
|
|
81
|
+
self._host = validated.host
|
|
82
|
+
self._port = validated.port
|
|
83
|
+
except Exception as e:
|
|
84
|
+
raise StorageConnectionError(
|
|
85
|
+
f'Failed to connect to SFTP: {e}',
|
|
86
|
+
details={'host': validated.host, 'port': validated.port},
|
|
87
|
+
) from e
|
|
88
|
+
|
|
89
|
+
def _get_full_path(self, path: str) -> UPath:
|
|
90
|
+
"""Get full UPath including root_path."""
|
|
91
|
+
normalized = self._normalize_path(path)
|
|
92
|
+
if self._root_path:
|
|
93
|
+
full_path = f'{self._root_path}/{normalized}' if normalized else self._root_path
|
|
94
|
+
else:
|
|
95
|
+
full_path = normalized or '/'
|
|
96
|
+
return self._base_upath / full_path.lstrip('/')
|
|
97
|
+
|
|
98
|
+
def upload(self, source: Path, target: str) -> str:
|
|
99
|
+
"""Upload a file via SFTP.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
source: Local file path to upload.
|
|
103
|
+
target: Target path on SFTP server.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
sftp:// URL of uploaded file.
|
|
107
|
+
"""
|
|
108
|
+
source_path = Path(source) if isinstance(source, str) else source
|
|
109
|
+
|
|
110
|
+
if not source_path.exists():
|
|
111
|
+
raise StorageNotFoundError(
|
|
112
|
+
f'Source file not found: {source_path}',
|
|
113
|
+
details={'source': str(source_path)},
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
target_upath = self._get_full_path(target)
|
|
117
|
+
|
|
118
|
+
try:
|
|
119
|
+
# Ensure parent directory exists
|
|
120
|
+
target_upath.parent.mkdir(parents=True, exist_ok=True)
|
|
121
|
+
|
|
122
|
+
with open(source_path, 'rb') as f:
|
|
123
|
+
target_upath.write_bytes(f.read())
|
|
124
|
+
except Exception as e:
|
|
125
|
+
raise StorageUploadError(
|
|
126
|
+
f'Failed to upload via SFTP: {e}',
|
|
127
|
+
details={'source': str(source_path), 'target': str(target_upath)},
|
|
128
|
+
) from e
|
|
129
|
+
|
|
130
|
+
return self.get_url(target)
|
|
131
|
+
|
|
132
|
+
def exists(self, target: str) -> bool:
|
|
133
|
+
"""Check if file exists on SFTP server.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
target: Path to check.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
True if exists, False otherwise.
|
|
140
|
+
"""
|
|
141
|
+
return self._get_full_path(target).exists()
|
|
142
|
+
|
|
143
|
+
def get_url(self, target: str) -> str:
|
|
144
|
+
"""Get sftp:// URL for target.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
target: Target path.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
sftp:// URL string.
|
|
151
|
+
"""
|
|
152
|
+
normalized = self._normalize_path(target)
|
|
153
|
+
if self._root_path:
|
|
154
|
+
full_path = f'{self._root_path}/{normalized}' if normalized else self._root_path
|
|
155
|
+
else:
|
|
156
|
+
full_path = normalized or '/'
|
|
157
|
+
|
|
158
|
+
if self._port != 22:
|
|
159
|
+
return f'sftp://{self._host}:{self._port}{full_path}'
|
|
160
|
+
return f'sftp://{self._host}{full_path}'
|
|
161
|
+
|
|
162
|
+
def get_pathlib(self, path: str) -> UPath:
|
|
163
|
+
"""Get UPath object for path.
|
|
164
|
+
|
|
165
|
+
Args:
|
|
166
|
+
path: Path relative to root_path.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
UPath object.
|
|
170
|
+
"""
|
|
171
|
+
return self._get_full_path(path)
|
|
172
|
+
|
|
173
|
+
def get_path_file_count(self, pathlib_obj: UPath) -> int:
|
|
174
|
+
"""Count files in SFTP path.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
pathlib_obj: UPath object from get_pathlib().
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
Number of files.
|
|
181
|
+
"""
|
|
182
|
+
return self._count_files(pathlib_obj)
|
|
183
|
+
|
|
184
|
+
def get_path_total_size(self, pathlib_obj: UPath) -> int:
|
|
185
|
+
"""Calculate total size of files in SFTP path.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
pathlib_obj: UPath object from get_pathlib().
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
Total size in bytes.
|
|
192
|
+
"""
|
|
193
|
+
return self._calculate_total_size(pathlib_obj)
|
|
194
|
+
|
|
195
|
+
def glob(self, pattern: str) -> list[UPath]:
|
|
196
|
+
"""Glob pattern matching on SFTP.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
pattern: Glob pattern.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
List of matching UPath objects.
|
|
203
|
+
"""
|
|
204
|
+
base = self._get_full_path('')
|
|
205
|
+
return list(base.glob(pattern))
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
__all__ = ['SFTPStorage']
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Storage provider registry with lazy loading."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Callable
|
|
6
|
+
|
|
7
|
+
from synapse_sdk.utils.storage.errors import StorageProviderNotFoundError
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from synapse_sdk.utils.storage import StorageProtocol
|
|
11
|
+
|
|
12
|
+
# Registry stores factory functions for lazy loading
|
|
13
|
+
_PROVIDER_REGISTRY: dict[str, Callable[[], type[StorageProtocol]]] = {}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _register_builtin_providers() -> None:
|
|
17
|
+
"""Register built-in storage providers with lazy loading."""
|
|
18
|
+
|
|
19
|
+
def local_factory() -> type:
|
|
20
|
+
from synapse_sdk.utils.storage.providers.local import LocalStorage
|
|
21
|
+
|
|
22
|
+
return LocalStorage
|
|
23
|
+
|
|
24
|
+
def s3_factory() -> type:
|
|
25
|
+
from synapse_sdk.utils.storage.providers.s3 import S3Storage
|
|
26
|
+
|
|
27
|
+
return S3Storage
|
|
28
|
+
|
|
29
|
+
def gcs_factory() -> type:
|
|
30
|
+
from synapse_sdk.utils.storage.providers.gcs import GCSStorage
|
|
31
|
+
|
|
32
|
+
return GCSStorage
|
|
33
|
+
|
|
34
|
+
def sftp_factory() -> type:
|
|
35
|
+
from synapse_sdk.utils.storage.providers.sftp import SFTPStorage
|
|
36
|
+
|
|
37
|
+
return SFTPStorage
|
|
38
|
+
|
|
39
|
+
def http_factory() -> type:
|
|
40
|
+
from synapse_sdk.utils.storage.providers.http import HTTPStorage
|
|
41
|
+
|
|
42
|
+
return HTTPStorage
|
|
43
|
+
|
|
44
|
+
# Local filesystem
|
|
45
|
+
_PROVIDER_REGISTRY['local'] = local_factory
|
|
46
|
+
_PROVIDER_REGISTRY['file_system'] = local_factory # backward compatibility
|
|
47
|
+
|
|
48
|
+
# S3-compatible
|
|
49
|
+
_PROVIDER_REGISTRY['s3'] = s3_factory
|
|
50
|
+
_PROVIDER_REGISTRY['amazon_s3'] = s3_factory
|
|
51
|
+
_PROVIDER_REGISTRY['minio'] = s3_factory
|
|
52
|
+
|
|
53
|
+
# Google Cloud Storage
|
|
54
|
+
_PROVIDER_REGISTRY['gcs'] = gcs_factory
|
|
55
|
+
_PROVIDER_REGISTRY['gs'] = gcs_factory
|
|
56
|
+
_PROVIDER_REGISTRY['gcp'] = gcs_factory
|
|
57
|
+
|
|
58
|
+
# SFTP
|
|
59
|
+
_PROVIDER_REGISTRY['sftp'] = sftp_factory
|
|
60
|
+
|
|
61
|
+
# HTTP
|
|
62
|
+
_PROVIDER_REGISTRY['http'] = http_factory
|
|
63
|
+
_PROVIDER_REGISTRY['https'] = http_factory
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def get_provider_class(provider: str) -> type[StorageProtocol]:
|
|
67
|
+
"""Get the storage provider class for the given provider name.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
provider: Provider name (e.g., 's3', 'gcs', 'local').
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Storage provider class.
|
|
74
|
+
|
|
75
|
+
Raises:
|
|
76
|
+
StorageProviderNotFoundError: If provider is not registered.
|
|
77
|
+
"""
|
|
78
|
+
if not _PROVIDER_REGISTRY:
|
|
79
|
+
_register_builtin_providers()
|
|
80
|
+
|
|
81
|
+
factory = _PROVIDER_REGISTRY.get(provider)
|
|
82
|
+
if not factory:
|
|
83
|
+
available = ', '.join(sorted(_PROVIDER_REGISTRY.keys()))
|
|
84
|
+
raise StorageProviderNotFoundError(
|
|
85
|
+
f"Provider '{provider}' not found. Available: {available}",
|
|
86
|
+
details={'provider': provider, 'available': list(_PROVIDER_REGISTRY.keys())},
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
return factory()
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def register_provider(name: str, factory: Callable[[], type[StorageProtocol]]) -> None:
|
|
93
|
+
"""Register a custom storage provider.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
name: Provider name to register.
|
|
97
|
+
factory: Factory function that returns the provider class.
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
>>> def custom_factory():
|
|
101
|
+
... from my_module import CustomStorage
|
|
102
|
+
... return CustomStorage
|
|
103
|
+
>>> register_provider('custom', custom_factory)
|
|
104
|
+
"""
|
|
105
|
+
if not _PROVIDER_REGISTRY:
|
|
106
|
+
_register_builtin_providers()
|
|
107
|
+
_PROVIDER_REGISTRY[name] = factory
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_registered_providers() -> list[str]:
|
|
111
|
+
"""Get list of registered provider names.
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
List of registered provider names.
|
|
115
|
+
"""
|
|
116
|
+
if not _PROVIDER_REGISTRY:
|
|
117
|
+
_register_builtin_providers()
|
|
118
|
+
return list(_PROVIDER_REGISTRY.keys())
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
__all__ = [
|
|
122
|
+
'get_provider_class',
|
|
123
|
+
'register_provider',
|
|
124
|
+
'get_registered_providers',
|
|
125
|
+
]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Generator
|
|
5
|
+
|
|
6
|
+
from synapse_sdk.clients.exceptions import ClientError
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def stream_websocket(
|
|
10
|
+
url: str,
|
|
11
|
+
headers: dict[str, str] | None = None,
|
|
12
|
+
timeout: float = 30.0,
|
|
13
|
+
) -> Generator[dict, None, None]:
|
|
14
|
+
"""Stream raw events from a WebSocket connection.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
url: WebSocket URL (ws:// or wss://).
|
|
18
|
+
headers: Optional headers dict.
|
|
19
|
+
timeout: Connection timeout in seconds.
|
|
20
|
+
|
|
21
|
+
Yields:
|
|
22
|
+
Parsed JSON events from the WebSocket.
|
|
23
|
+
|
|
24
|
+
Raises:
|
|
25
|
+
ClientError: On connection or protocol errors.
|
|
26
|
+
"""
|
|
27
|
+
try:
|
|
28
|
+
import websocket
|
|
29
|
+
except ImportError:
|
|
30
|
+
raise ClientError(500, 'websocket-client package required for streaming')
|
|
31
|
+
|
|
32
|
+
header_list = [f'{k}: {v}' for k, v in (headers or {}).items()]
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
ws = websocket.create_connection(url, header=header_list, timeout=timeout)
|
|
36
|
+
except websocket.WebSocketException as e:
|
|
37
|
+
raise ClientError(503, f'WebSocket connection failed: {e}')
|
|
38
|
+
except Exception as e:
|
|
39
|
+
raise ClientError(503, f'Connection error: {e}')
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
while True:
|
|
43
|
+
try:
|
|
44
|
+
data = ws.recv()
|
|
45
|
+
except websocket.WebSocketTimeoutException:
|
|
46
|
+
break
|
|
47
|
+
except websocket.WebSocketConnectionClosedException:
|
|
48
|
+
break
|
|
49
|
+
|
|
50
|
+
if not data:
|
|
51
|
+
break
|
|
52
|
+
|
|
53
|
+
try:
|
|
54
|
+
event = json.loads(data)
|
|
55
|
+
except json.JSONDecodeError:
|
|
56
|
+
event = {'message': data}
|
|
57
|
+
|
|
58
|
+
yield event
|
|
59
|
+
finally:
|
|
60
|
+
ws.close()
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def stream_websocket_logs(
|
|
64
|
+
url: str,
|
|
65
|
+
headers: dict[str, str] | None = None,
|
|
66
|
+
timeout: float = 30.0,
|
|
67
|
+
) -> Generator[str, None, None]:
|
|
68
|
+
"""Stream log messages from a WebSocket connection.
|
|
69
|
+
|
|
70
|
+
Handles the standard log streaming protocol:
|
|
71
|
+
- 'log' events: yields the message
|
|
72
|
+
- 'error' events: raises ClientError
|
|
73
|
+
- 'complete' events: stops iteration
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
url: WebSocket URL (ws:// or wss://).
|
|
77
|
+
headers: Optional headers dict.
|
|
78
|
+
timeout: Connection timeout in seconds.
|
|
79
|
+
|
|
80
|
+
Yields:
|
|
81
|
+
Log message strings.
|
|
82
|
+
|
|
83
|
+
Raises:
|
|
84
|
+
ClientError: On connection, protocol, or server errors.
|
|
85
|
+
"""
|
|
86
|
+
for event in stream_websocket(url, headers, timeout):
|
|
87
|
+
match event.get('type'):
|
|
88
|
+
case 'error':
|
|
89
|
+
raise ClientError(500, event.get('message', 'Unknown error'))
|
|
90
|
+
case 'complete':
|
|
91
|
+
return
|
|
92
|
+
case _:
|
|
93
|
+
if msg := event.get('message'):
|
|
94
|
+
yield msg
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def http_to_ws_url(url: str) -> str:
|
|
98
|
+
"""Convert HTTP URL to WebSocket URL."""
|
|
99
|
+
return url.replace('http://', 'ws://').replace('https://', 'wss://')
|