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,168 @@
|
|
|
1
|
+
"""Google Cloud 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 GCSStorageConfig
|
|
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 GCSStorage(_BaseStorageMixin):
|
|
21
|
+
"""Storage provider for Google Cloud Storage.
|
|
22
|
+
|
|
23
|
+
Requires: universal-pathlib[gcs] (pip install universal-pathlib[gcs])
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
config: Configuration dict with GCS credentials.
|
|
27
|
+
|
|
28
|
+
Example:
|
|
29
|
+
>>> storage = GCSStorage({
|
|
30
|
+
... 'bucket_name': 'my-bucket',
|
|
31
|
+
... 'credentials': '/path/to/service-account.json',
|
|
32
|
+
... })
|
|
33
|
+
>>> storage.upload(Path('/tmp/file.txt'), 'data/file.txt')
|
|
34
|
+
'gs://my-bucket/data/file.txt'
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, config: dict[str, Any]):
|
|
38
|
+
try:
|
|
39
|
+
from upath import UPath
|
|
40
|
+
except ImportError as e:
|
|
41
|
+
raise ImportError(
|
|
42
|
+
'GCSStorage requires universal-pathlib[gcs]. Install with: pip install universal-pathlib[gcs]'
|
|
43
|
+
) from e
|
|
44
|
+
|
|
45
|
+
validated = GCSStorageConfig.model_validate(config)
|
|
46
|
+
|
|
47
|
+
self._bucket_name = validated.bucket_name
|
|
48
|
+
upath_kwargs: dict[str, Any] = {'token': validated.credentials}
|
|
49
|
+
if validated.project:
|
|
50
|
+
upath_kwargs['project'] = validated.project
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
self._upath = UPath(
|
|
54
|
+
f'gs://{validated.bucket_name}',
|
|
55
|
+
**upath_kwargs,
|
|
56
|
+
)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
raise StorageConnectionError(
|
|
59
|
+
f'Failed to connect to GCS: {e}',
|
|
60
|
+
details={'bucket': validated.bucket_name},
|
|
61
|
+
) from e
|
|
62
|
+
|
|
63
|
+
def upload(self, source: Path, target: str) -> str:
|
|
64
|
+
"""Upload a file to GCS.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
source: Local file path to upload.
|
|
68
|
+
target: Target path in GCS bucket.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
gs:// URL of uploaded file.
|
|
72
|
+
"""
|
|
73
|
+
source_path = Path(source) if isinstance(source, str) else source
|
|
74
|
+
|
|
75
|
+
if not source_path.exists():
|
|
76
|
+
raise StorageNotFoundError(
|
|
77
|
+
f'Source file not found: {source_path}',
|
|
78
|
+
details={'source': str(source_path)},
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
target_path = self._normalize_path(target)
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
with open(source_path, 'rb') as f:
|
|
85
|
+
(self._upath / target_path).write_bytes(f.read())
|
|
86
|
+
except Exception as e:
|
|
87
|
+
raise StorageUploadError(
|
|
88
|
+
f'Failed to upload to GCS: {e}',
|
|
89
|
+
details={'source': str(source_path), 'target': target_path},
|
|
90
|
+
) from e
|
|
91
|
+
|
|
92
|
+
return self.get_url(target)
|
|
93
|
+
|
|
94
|
+
def exists(self, target: str) -> bool:
|
|
95
|
+
"""Check if file exists in GCS.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
target: Path to check.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
True if exists, False otherwise.
|
|
102
|
+
"""
|
|
103
|
+
target_path = self._normalize_path(target)
|
|
104
|
+
return (self._upath / target_path).exists()
|
|
105
|
+
|
|
106
|
+
def get_url(self, target: str) -> str:
|
|
107
|
+
"""Get gs:// URL for target.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
target: Target path.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
gs:// URL string.
|
|
114
|
+
"""
|
|
115
|
+
target_path = self._normalize_path(target)
|
|
116
|
+
if target_path:
|
|
117
|
+
return f'gs://{self._bucket_name}/{target_path}'
|
|
118
|
+
return f'gs://{self._bucket_name}'
|
|
119
|
+
|
|
120
|
+
def get_pathlib(self, path: str) -> UPath:
|
|
121
|
+
"""Get UPath object for path.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
path: Path relative to bucket root.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
UPath object.
|
|
128
|
+
"""
|
|
129
|
+
normalized = self._normalize_path(path)
|
|
130
|
+
if not normalized:
|
|
131
|
+
return self._upath
|
|
132
|
+
return self._upath / normalized
|
|
133
|
+
|
|
134
|
+
def get_path_file_count(self, pathlib_obj: UPath) -> int:
|
|
135
|
+
"""Count files in GCS path.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
pathlib_obj: UPath object from get_pathlib().
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
Number of files.
|
|
142
|
+
"""
|
|
143
|
+
return self._count_files(pathlib_obj)
|
|
144
|
+
|
|
145
|
+
def get_path_total_size(self, pathlib_obj: UPath) -> int:
|
|
146
|
+
"""Calculate total size of files in GCS path.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
pathlib_obj: UPath object from get_pathlib().
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
Total size in bytes.
|
|
153
|
+
"""
|
|
154
|
+
return self._calculate_total_size(pathlib_obj)
|
|
155
|
+
|
|
156
|
+
def glob(self, pattern: str) -> list[UPath]:
|
|
157
|
+
"""Glob pattern matching in GCS.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
pattern: Glob pattern.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
List of matching UPath objects.
|
|
164
|
+
"""
|
|
165
|
+
return list(self._upath.glob(pattern))
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
__all__ = ['GCSStorage']
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
"""HTTP storage provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
from urllib.parse import urljoin
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
|
|
12
|
+
from synapse_sdk.utils.storage.config import HTTPStorageConfig
|
|
13
|
+
from synapse_sdk.utils.storage.errors import (
|
|
14
|
+
StorageError,
|
|
15
|
+
StorageNotFoundError,
|
|
16
|
+
StorageUploadError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class HTTPStorage:
|
|
21
|
+
"""Storage provider for HTTP file servers.
|
|
22
|
+
|
|
23
|
+
Note: This provider has limited functionality as HTTP servers typically
|
|
24
|
+
don't support directory listing. File counting and size calculation
|
|
25
|
+
are not supported.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
config: Configuration dict with HTTP server details.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
>>> storage = HTTPStorage({
|
|
32
|
+
... 'base_url': 'https://files.example.com/uploads/',
|
|
33
|
+
... 'timeout': 60,
|
|
34
|
+
... })
|
|
35
|
+
>>> storage.exists('data/file.txt')
|
|
36
|
+
True
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, config: dict[str, Any]):
|
|
40
|
+
validated = HTTPStorageConfig.model_validate(config)
|
|
41
|
+
|
|
42
|
+
self._base_url = validated.base_url
|
|
43
|
+
self._timeout = validated.timeout
|
|
44
|
+
self._headers = validated.headers
|
|
45
|
+
|
|
46
|
+
# Setup session for connection pooling
|
|
47
|
+
self._session = requests.Session()
|
|
48
|
+
if self._headers:
|
|
49
|
+
self._session.headers.update(self._headers)
|
|
50
|
+
|
|
51
|
+
def _get_full_url(self, path: str) -> str:
|
|
52
|
+
"""Get full URL for a path."""
|
|
53
|
+
if path.startswith('/'):
|
|
54
|
+
path = path[1:]
|
|
55
|
+
return urljoin(self._base_url, path)
|
|
56
|
+
|
|
57
|
+
def upload(self, source: Path, target: str) -> str:
|
|
58
|
+
"""Upload a file to HTTP server.
|
|
59
|
+
|
|
60
|
+
Note: Requires server to support PUT or POST for file uploads.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
source: Local file path to upload.
|
|
64
|
+
target: Target path on server.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
URL of uploaded file.
|
|
68
|
+
"""
|
|
69
|
+
source_path = Path(source) if isinstance(source, str) else source
|
|
70
|
+
|
|
71
|
+
if not source_path.exists():
|
|
72
|
+
raise StorageNotFoundError(
|
|
73
|
+
f'Source file not found: {source_path}',
|
|
74
|
+
details={'source': str(source_path)},
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
url = self._get_full_url(target)
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
with open(source_path, 'rb') as f:
|
|
81
|
+
files = {'file': (os.path.basename(str(source_path)), f)}
|
|
82
|
+
|
|
83
|
+
# Try PUT first, fallback to POST
|
|
84
|
+
response = self._session.put(url, files=files, timeout=self._timeout)
|
|
85
|
+
|
|
86
|
+
if response.status_code == 405:
|
|
87
|
+
f.seek(0)
|
|
88
|
+
response = self._session.post(url, files=files, timeout=self._timeout)
|
|
89
|
+
|
|
90
|
+
response.raise_for_status()
|
|
91
|
+
except requests.RequestException as e:
|
|
92
|
+
raise StorageUploadError(
|
|
93
|
+
f'Failed to upload to HTTP server: {e}',
|
|
94
|
+
details={'url': url},
|
|
95
|
+
) from e
|
|
96
|
+
|
|
97
|
+
return url
|
|
98
|
+
|
|
99
|
+
def exists(self, target: str) -> bool:
|
|
100
|
+
"""Check if file exists on HTTP server.
|
|
101
|
+
|
|
102
|
+
Uses HEAD request to check existence.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
target: Path to check.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
True if file exists (HTTP 200), False otherwise.
|
|
109
|
+
"""
|
|
110
|
+
url = self._get_full_url(target)
|
|
111
|
+
|
|
112
|
+
try:
|
|
113
|
+
response = self._session.head(url, timeout=self._timeout)
|
|
114
|
+
return response.status_code == 200
|
|
115
|
+
except requests.RequestException:
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
def get_url(self, target: str) -> str:
|
|
119
|
+
"""Get full URL for target.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
target: Target path.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Full HTTP URL.
|
|
126
|
+
"""
|
|
127
|
+
return self._get_full_url(target)
|
|
128
|
+
|
|
129
|
+
def get_pathlib(self, path: str) -> HTTPPath:
|
|
130
|
+
"""Get HTTPPath object for path.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
path: Path on server.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
HTTPPath object (pathlib-like interface).
|
|
137
|
+
"""
|
|
138
|
+
return HTTPPath(self, path)
|
|
139
|
+
|
|
140
|
+
def get_path_file_count(self, pathlib_obj: HTTPPath) -> int:
|
|
141
|
+
"""Not supported for HTTP storage.
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
StorageError: Always, as HTTP servers don't support directory listing.
|
|
145
|
+
"""
|
|
146
|
+
raise StorageError(
|
|
147
|
+
'File counting not supported for HTTP storage',
|
|
148
|
+
details={'reason': 'HTTP servers do not provide directory listing'},
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
def get_path_total_size(self, pathlib_obj: HTTPPath) -> int:
|
|
152
|
+
"""Not supported for HTTP storage.
|
|
153
|
+
|
|
154
|
+
Raises:
|
|
155
|
+
StorageError: Always, as HTTP servers don't support directory listing.
|
|
156
|
+
"""
|
|
157
|
+
raise StorageError(
|
|
158
|
+
'Size calculation not supported for HTTP storage',
|
|
159
|
+
details={'reason': 'HTTP servers do not provide directory listing'},
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class HTTPPath:
|
|
164
|
+
"""Pathlib-like interface for HTTP paths.
|
|
165
|
+
|
|
166
|
+
Provides a subset of pathlib.Path functionality for HTTP resources.
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
def __init__(self, storage: HTTPStorage, path: str):
|
|
170
|
+
self._storage = storage
|
|
171
|
+
self._path = path.strip('/')
|
|
172
|
+
|
|
173
|
+
def __str__(self) -> str:
|
|
174
|
+
return self._storage.get_url(self._path)
|
|
175
|
+
|
|
176
|
+
def __repr__(self) -> str:
|
|
177
|
+
return f"HTTPPath('{self}')"
|
|
178
|
+
|
|
179
|
+
def __truediv__(self, other: str) -> HTTPPath:
|
|
180
|
+
"""Join paths using / operator."""
|
|
181
|
+
new_path = f'{self._path}/{other}' if self._path else str(other)
|
|
182
|
+
return HTTPPath(self._storage, new_path)
|
|
183
|
+
|
|
184
|
+
def joinuri(self, *parts: str) -> HTTPPath:
|
|
185
|
+
"""Join path parts."""
|
|
186
|
+
all_parts = [self._path] + [str(p).strip('/') for p in parts]
|
|
187
|
+
new_path = '/'.join(p for p in all_parts if p)
|
|
188
|
+
return HTTPPath(self._storage, new_path)
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def name(self) -> str:
|
|
192
|
+
"""Get the final component of the path."""
|
|
193
|
+
return os.path.basename(self._path)
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def parent(self) -> HTTPPath:
|
|
197
|
+
"""Get the parent directory."""
|
|
198
|
+
parent_path = os.path.dirname(self._path)
|
|
199
|
+
return HTTPPath(self._storage, parent_path)
|
|
200
|
+
|
|
201
|
+
def exists(self) -> bool:
|
|
202
|
+
"""Check if this path exists."""
|
|
203
|
+
return self._storage.exists(self._path)
|
|
204
|
+
|
|
205
|
+
def is_file(self) -> bool:
|
|
206
|
+
"""Check if this path is a file (assumes exists = is_file for HTTP)."""
|
|
207
|
+
return self.exists()
|
|
208
|
+
|
|
209
|
+
def is_dir(self) -> bool:
|
|
210
|
+
"""Check if this path is a directory.
|
|
211
|
+
|
|
212
|
+
Note: HTTP servers don't typically distinguish directories.
|
|
213
|
+
This always returns False.
|
|
214
|
+
"""
|
|
215
|
+
return False
|
|
216
|
+
|
|
217
|
+
def read_bytes(self) -> bytes:
|
|
218
|
+
"""Read file contents as bytes."""
|
|
219
|
+
url = self._storage.get_url(self._path)
|
|
220
|
+
response = self._storage._session.get(url, timeout=self._storage._timeout)
|
|
221
|
+
response.raise_for_status()
|
|
222
|
+
return response.content
|
|
223
|
+
|
|
224
|
+
def read_text(self, encoding: str = 'utf-8') -> str:
|
|
225
|
+
"""Read file contents as text."""
|
|
226
|
+
return self.read_bytes().decode(encoding)
|
|
227
|
+
|
|
228
|
+
def stat(self) -> HTTPStat:
|
|
229
|
+
"""Get file statistics.
|
|
230
|
+
|
|
231
|
+
Note: Only st_size is populated via Content-Length header.
|
|
232
|
+
"""
|
|
233
|
+
url = self._storage.get_url(self._path)
|
|
234
|
+
response = self._storage._session.head(url, timeout=self._storage._timeout)
|
|
235
|
+
response.raise_for_status()
|
|
236
|
+
|
|
237
|
+
content_length = response.headers.get('Content-Length')
|
|
238
|
+
size = int(content_length) if content_length else 0
|
|
239
|
+
|
|
240
|
+
return HTTPStat(st_size=size)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class HTTPStat:
|
|
244
|
+
"""Minimal stat result for HTTP files."""
|
|
245
|
+
|
|
246
|
+
def __init__(self, st_size: int = 0):
|
|
247
|
+
self.st_size = st_size
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
__all__ = ['HTTPStorage', 'HTTPPath']
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Local filesystem storage provider."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from synapse_sdk.utils.storage.config import LocalStorageConfig
|
|
10
|
+
from synapse_sdk.utils.storage.errors import StorageNotFoundError, StorageUploadError
|
|
11
|
+
from synapse_sdk.utils.storage.providers.base import _BaseStorageMixin
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LocalStorage(_BaseStorageMixin):
|
|
15
|
+
"""Storage provider for local filesystem.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
config: Configuration dict with 'location' key.
|
|
19
|
+
|
|
20
|
+
Example:
|
|
21
|
+
>>> storage = LocalStorage({'location': '/data'})
|
|
22
|
+
>>> storage.upload(Path('/tmp/file.txt'), 'uploads/file.txt')
|
|
23
|
+
'file:///data/uploads/file.txt'
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(self, config: dict[str, Any]):
|
|
27
|
+
validated = LocalStorageConfig.model_validate(config)
|
|
28
|
+
self.base_path = Path(validated.location)
|
|
29
|
+
|
|
30
|
+
def upload(self, source: Path, target: str) -> str:
|
|
31
|
+
"""Upload a file from source to target location.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
source: Path to source file.
|
|
35
|
+
target: Target path relative to base path.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
file:// URL of uploaded file.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
StorageNotFoundError: If source file doesn't exist.
|
|
42
|
+
StorageUploadError: If copy operation fails.
|
|
43
|
+
"""
|
|
44
|
+
source_path = Path(source) if isinstance(source, str) else source
|
|
45
|
+
|
|
46
|
+
if not source_path.exists():
|
|
47
|
+
raise StorageNotFoundError(
|
|
48
|
+
f'Source file not found: {source_path}',
|
|
49
|
+
details={'source': str(source_path)},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
target_path = self.base_path / self._normalize_path(target)
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
56
|
+
shutil.copy2(source_path, target_path)
|
|
57
|
+
except OSError as e:
|
|
58
|
+
raise StorageUploadError(
|
|
59
|
+
f'Failed to upload file: {e}',
|
|
60
|
+
details={'source': str(source_path), 'target': str(target_path)},
|
|
61
|
+
) from e
|
|
62
|
+
|
|
63
|
+
return self.get_url(target)
|
|
64
|
+
|
|
65
|
+
def exists(self, target: str) -> bool:
|
|
66
|
+
"""Check if target file exists.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
target: Target path relative to base path.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
True if file exists, False otherwise.
|
|
73
|
+
"""
|
|
74
|
+
target_path = self.base_path / self._normalize_path(target)
|
|
75
|
+
return target_path.exists()
|
|
76
|
+
|
|
77
|
+
def get_url(self, target: str) -> str:
|
|
78
|
+
"""Get file:// URL for target file.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
target: Target path relative to base path.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
file:// URL string.
|
|
85
|
+
"""
|
|
86
|
+
target_path = self.base_path / self._normalize_path(target)
|
|
87
|
+
return f'file://{target_path.absolute()}'
|
|
88
|
+
|
|
89
|
+
def get_pathlib(self, path: str) -> Path:
|
|
90
|
+
"""Get pathlib.Path object for the path.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
path: Path relative to storage root.
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
pathlib.Path object.
|
|
97
|
+
"""
|
|
98
|
+
normalized = self._normalize_path(path)
|
|
99
|
+
if not normalized:
|
|
100
|
+
return self.base_path
|
|
101
|
+
return self.base_path / normalized
|
|
102
|
+
|
|
103
|
+
def get_path_file_count(self, pathlib_obj: Path) -> int:
|
|
104
|
+
"""Get file count in the path.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
pathlib_obj: Path object from get_pathlib().
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Number of files.
|
|
111
|
+
"""
|
|
112
|
+
return self._count_files(pathlib_obj)
|
|
113
|
+
|
|
114
|
+
def get_path_total_size(self, pathlib_obj: Path) -> int:
|
|
115
|
+
"""Get total size of files in the path.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
pathlib_obj: Path object from get_pathlib().
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Total size in bytes.
|
|
122
|
+
"""
|
|
123
|
+
return self._calculate_total_size(pathlib_obj)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
__all__ = ['LocalStorage']
|