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,449 @@
|
|
|
1
|
+
"""Archive utilities for creating and extracting ZIP files."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import fnmatch
|
|
6
|
+
import subprocess
|
|
7
|
+
import zipfile
|
|
8
|
+
from collections.abc import Callable, Iterable
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Literal
|
|
11
|
+
|
|
12
|
+
# Progress callback signature: (current_file_index, total_files)
|
|
13
|
+
ProgressCallback = Callable[[int, int], None]
|
|
14
|
+
|
|
15
|
+
# Compression method mapping
|
|
16
|
+
COMPRESSION_METHODS = {
|
|
17
|
+
'stored': zipfile.ZIP_STORED,
|
|
18
|
+
'deflated': zipfile.ZIP_DEFLATED,
|
|
19
|
+
'bzip2': zipfile.ZIP_BZIP2,
|
|
20
|
+
'lzma': zipfile.ZIP_LZMA,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ArchiveFilter:
|
|
25
|
+
"""Filter for selecting files to include in archive.
|
|
26
|
+
|
|
27
|
+
Supports glob patterns for include/exclude filtering.
|
|
28
|
+
Default excludes common non-essential directories and files.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
>>> filter = ArchiveFilter.from_patterns(exclude=['*.pyc', '__pycache__'])
|
|
32
|
+
>>> filter.should_include(Path('src/main.py'), Path('/project'))
|
|
33
|
+
True
|
|
34
|
+
>>> filter.should_include(Path('__pycache__/cache.pyc'), Path('/project'))
|
|
35
|
+
False
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
DEFAULT_EXCLUDES: frozenset[str] = frozenset({
|
|
39
|
+
# Python
|
|
40
|
+
'__pycache__',
|
|
41
|
+
'*.pyc',
|
|
42
|
+
'*.pyo',
|
|
43
|
+
'*.pyd',
|
|
44
|
+
'.python-version',
|
|
45
|
+
'*.egg-info',
|
|
46
|
+
'*.egg',
|
|
47
|
+
# Virtual environments
|
|
48
|
+
'.venv',
|
|
49
|
+
'venv',
|
|
50
|
+
'.env',
|
|
51
|
+
'env',
|
|
52
|
+
# Version control
|
|
53
|
+
'.git',
|
|
54
|
+
'.gitignore',
|
|
55
|
+
'.gitattributes',
|
|
56
|
+
'.svn',
|
|
57
|
+
'.hg',
|
|
58
|
+
# IDE/Editor
|
|
59
|
+
'.idea',
|
|
60
|
+
'.vscode',
|
|
61
|
+
'*.swp',
|
|
62
|
+
'*.swo',
|
|
63
|
+
'.DS_Store',
|
|
64
|
+
'Thumbs.db',
|
|
65
|
+
# Build artifacts
|
|
66
|
+
'dist',
|
|
67
|
+
'build',
|
|
68
|
+
'node_modules',
|
|
69
|
+
# Cache
|
|
70
|
+
'.mypy_cache',
|
|
71
|
+
'.pytest_cache',
|
|
72
|
+
'.ruff_cache',
|
|
73
|
+
'.cache',
|
|
74
|
+
'.tox',
|
|
75
|
+
# Test coverage
|
|
76
|
+
'.coverage',
|
|
77
|
+
'htmlcov',
|
|
78
|
+
# Logs
|
|
79
|
+
'*.log',
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
include_patterns: frozenset[str] | None = None,
|
|
85
|
+
exclude_patterns: frozenset[str] | None = None,
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Initialize archive filter.
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
include_patterns: Glob patterns for files to include (None = all).
|
|
91
|
+
exclude_patterns: Glob patterns for files to exclude.
|
|
92
|
+
"""
|
|
93
|
+
self._include_patterns = include_patterns
|
|
94
|
+
self._exclude_patterns = exclude_patterns or self.DEFAULT_EXCLUDES
|
|
95
|
+
|
|
96
|
+
@classmethod
|
|
97
|
+
def from_patterns(
|
|
98
|
+
cls,
|
|
99
|
+
include: Iterable[str] | None = None,
|
|
100
|
+
exclude: Iterable[str] | None = None,
|
|
101
|
+
*,
|
|
102
|
+
use_defaults: bool = True,
|
|
103
|
+
) -> ArchiveFilter:
|
|
104
|
+
"""Create filter from glob patterns.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
include: Patterns for files to include.
|
|
108
|
+
exclude: Patterns for files to exclude.
|
|
109
|
+
use_defaults: Include DEFAULT_EXCLUDES in exclude patterns.
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
Configured ArchiveFilter instance.
|
|
113
|
+
"""
|
|
114
|
+
include_set = frozenset(include) if include else None
|
|
115
|
+
|
|
116
|
+
exclude_set: frozenset[str]
|
|
117
|
+
if exclude:
|
|
118
|
+
if use_defaults:
|
|
119
|
+
exclude_set = frozenset(exclude) | cls.DEFAULT_EXCLUDES
|
|
120
|
+
else:
|
|
121
|
+
exclude_set = frozenset(exclude)
|
|
122
|
+
elif use_defaults:
|
|
123
|
+
exclude_set = cls.DEFAULT_EXCLUDES
|
|
124
|
+
else:
|
|
125
|
+
exclude_set = frozenset()
|
|
126
|
+
|
|
127
|
+
return cls(include_patterns=include_set, exclude_patterns=exclude_set)
|
|
128
|
+
|
|
129
|
+
def _matches_any_pattern(self, path_str: str, patterns: frozenset[str]) -> bool:
|
|
130
|
+
"""Check if path matches any of the patterns."""
|
|
131
|
+
# Check the full path and each component
|
|
132
|
+
path_parts = Path(path_str).parts
|
|
133
|
+
|
|
134
|
+
for pattern in patterns:
|
|
135
|
+
# Match against full path
|
|
136
|
+
if fnmatch.fnmatch(path_str, pattern):
|
|
137
|
+
return True
|
|
138
|
+
# Match against filename only
|
|
139
|
+
if fnmatch.fnmatch(path_parts[-1], pattern):
|
|
140
|
+
return True
|
|
141
|
+
# Match against any path component (for directory patterns)
|
|
142
|
+
for part in path_parts:
|
|
143
|
+
if fnmatch.fnmatch(part, pattern):
|
|
144
|
+
return True
|
|
145
|
+
return False
|
|
146
|
+
|
|
147
|
+
def should_include(self, path: Path, relative_to: Path) -> bool:
|
|
148
|
+
"""Check if path should be included in archive.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
path: Absolute path to check.
|
|
152
|
+
relative_to: Base path for relative path calculation.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
True if file should be included.
|
|
156
|
+
"""
|
|
157
|
+
# Only include files, not directories
|
|
158
|
+
if path.is_dir():
|
|
159
|
+
return False
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
rel_path = str(path.relative_to(relative_to))
|
|
163
|
+
except ValueError:
|
|
164
|
+
rel_path = str(path)
|
|
165
|
+
|
|
166
|
+
# Check exclude patterns first
|
|
167
|
+
if self._matches_any_pattern(rel_path, self._exclude_patterns):
|
|
168
|
+
return False
|
|
169
|
+
|
|
170
|
+
# If include patterns specified, file must match at least one
|
|
171
|
+
if self._include_patterns is not None:
|
|
172
|
+
return self._matches_any_pattern(rel_path, self._include_patterns)
|
|
173
|
+
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def create_archive(
|
|
178
|
+
source_path: str | Path,
|
|
179
|
+
archive_path: str | Path,
|
|
180
|
+
*,
|
|
181
|
+
filter: ArchiveFilter | None = None,
|
|
182
|
+
compression: Literal['stored', 'deflated', 'bzip2', 'lzma'] = 'deflated',
|
|
183
|
+
compression_level: int = 6,
|
|
184
|
+
progress_callback: ProgressCallback | None = None,
|
|
185
|
+
) -> Path:
|
|
186
|
+
"""Create a ZIP archive from source directory.
|
|
187
|
+
|
|
188
|
+
Uses pure Python zipfile module for cross-platform compatibility
|
|
189
|
+
and security (no shell execution).
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
source_path: Directory to archive.
|
|
193
|
+
archive_path: Output ZIP file path.
|
|
194
|
+
filter: File filter (defaults to ArchiveFilter with DEFAULT_EXCLUDES).
|
|
195
|
+
compression: Compression method.
|
|
196
|
+
compression_level: Compression level (1-9 for deflated, ignored for others).
|
|
197
|
+
progress_callback: Optional callback for progress updates.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Path to created archive.
|
|
201
|
+
|
|
202
|
+
Raises:
|
|
203
|
+
FileNotFoundError: If source_path does not exist.
|
|
204
|
+
NotADirectoryError: If source_path is not a directory.
|
|
205
|
+
|
|
206
|
+
Example:
|
|
207
|
+
>>> archive_path = create_archive('/path/to/project', '/tmp/project.zip')
|
|
208
|
+
"""
|
|
209
|
+
source = Path(source_path).resolve()
|
|
210
|
+
archive = Path(archive_path).resolve()
|
|
211
|
+
|
|
212
|
+
if not source.exists():
|
|
213
|
+
raise FileNotFoundError(f'Source path not found: {source}')
|
|
214
|
+
if not source.is_dir():
|
|
215
|
+
raise NotADirectoryError(f'Source path is not a directory: {source}')
|
|
216
|
+
|
|
217
|
+
# Ensure archive parent directory exists
|
|
218
|
+
archive.parent.mkdir(parents=True, exist_ok=True)
|
|
219
|
+
|
|
220
|
+
# Use default filter if not provided
|
|
221
|
+
if filter is None:
|
|
222
|
+
filter = ArchiveFilter.from_patterns()
|
|
223
|
+
|
|
224
|
+
# Collect files to archive
|
|
225
|
+
files_to_archive: list[Path] = []
|
|
226
|
+
for file_path in source.rglob('*'):
|
|
227
|
+
if filter.should_include(file_path, source):
|
|
228
|
+
files_to_archive.append(file_path)
|
|
229
|
+
|
|
230
|
+
total_files = len(files_to_archive)
|
|
231
|
+
compression_method = COMPRESSION_METHODS[compression]
|
|
232
|
+
|
|
233
|
+
# Set compression level for deflated
|
|
234
|
+
compresslevel = compression_level if compression == 'deflated' else None
|
|
235
|
+
|
|
236
|
+
with zipfile.ZipFile(
|
|
237
|
+
archive,
|
|
238
|
+
mode='w',
|
|
239
|
+
compression=compression_method,
|
|
240
|
+
compresslevel=compresslevel,
|
|
241
|
+
) as zf:
|
|
242
|
+
for idx, file_path in enumerate(files_to_archive):
|
|
243
|
+
rel_path = file_path.relative_to(source)
|
|
244
|
+
zf.write(file_path, rel_path)
|
|
245
|
+
|
|
246
|
+
if progress_callback:
|
|
247
|
+
progress_callback(idx + 1, total_files)
|
|
248
|
+
|
|
249
|
+
return archive
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def create_archive_from_git(
|
|
253
|
+
source_path: str | Path,
|
|
254
|
+
archive_path: str | Path,
|
|
255
|
+
*,
|
|
256
|
+
include_untracked: bool = True,
|
|
257
|
+
compression: Literal['stored', 'deflated', 'bzip2', 'lzma'] = 'deflated',
|
|
258
|
+
compression_level: int = 6,
|
|
259
|
+
progress_callback: ProgressCallback | None = None,
|
|
260
|
+
) -> Path:
|
|
261
|
+
"""Create archive from git-tracked files only.
|
|
262
|
+
|
|
263
|
+
Uses `git ls-files` to determine which files to include,
|
|
264
|
+
but creates the archive with pure Python zipfile (no shell=True).
|
|
265
|
+
|
|
266
|
+
Args:
|
|
267
|
+
source_path: Git repository directory.
|
|
268
|
+
archive_path: Output ZIP file path.
|
|
269
|
+
include_untracked: Include untracked files (--others --exclude-standard).
|
|
270
|
+
compression: Compression method.
|
|
271
|
+
compression_level: Compression level (1-9 for deflated).
|
|
272
|
+
progress_callback: Optional callback for progress updates.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Path to created archive.
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
FileNotFoundError: If source_path does not exist.
|
|
279
|
+
RuntimeError: If not a git repository or git command fails.
|
|
280
|
+
|
|
281
|
+
Example:
|
|
282
|
+
>>> archive_path = create_archive_from_git('/path/to/repo', '/tmp/repo.zip')
|
|
283
|
+
"""
|
|
284
|
+
source = Path(source_path).resolve()
|
|
285
|
+
archive = Path(archive_path).resolve()
|
|
286
|
+
|
|
287
|
+
if not source.exists():
|
|
288
|
+
raise FileNotFoundError(f'Source path not found: {source}')
|
|
289
|
+
|
|
290
|
+
# Check if it's a git repository
|
|
291
|
+
git_dir = source / '.git'
|
|
292
|
+
if not git_dir.exists():
|
|
293
|
+
raise RuntimeError(f'Not a git repository: {source}')
|
|
294
|
+
|
|
295
|
+
# Ensure archive parent directory exists
|
|
296
|
+
archive.parent.mkdir(parents=True, exist_ok=True)
|
|
297
|
+
|
|
298
|
+
# Build git ls-files command (no shell=True)
|
|
299
|
+
git_cmd = ['git', 'ls-files', '--cached']
|
|
300
|
+
|
|
301
|
+
if include_untracked:
|
|
302
|
+
git_cmd.extend(['--others', '--exclude-standard'])
|
|
303
|
+
|
|
304
|
+
# Run git ls-files
|
|
305
|
+
try:
|
|
306
|
+
result = subprocess.run(
|
|
307
|
+
git_cmd,
|
|
308
|
+
cwd=source,
|
|
309
|
+
capture_output=True,
|
|
310
|
+
text=True,
|
|
311
|
+
check=True,
|
|
312
|
+
)
|
|
313
|
+
except subprocess.CalledProcessError as e:
|
|
314
|
+
raise RuntimeError(f'git ls-files failed: {e.stderr}') from e
|
|
315
|
+
except FileNotFoundError as e:
|
|
316
|
+
raise RuntimeError('git command not found. Is git installed?') from e
|
|
317
|
+
|
|
318
|
+
# Parse output - each line is a file path
|
|
319
|
+
files_str = result.stdout.strip()
|
|
320
|
+
if not files_str:
|
|
321
|
+
# Empty repository or no files
|
|
322
|
+
file_list: list[str] = []
|
|
323
|
+
else:
|
|
324
|
+
file_list = files_str.split('\n')
|
|
325
|
+
|
|
326
|
+
# Filter out any empty strings
|
|
327
|
+
file_list = [f for f in file_list if f]
|
|
328
|
+
|
|
329
|
+
total_files = len(file_list)
|
|
330
|
+
compression_method = COMPRESSION_METHODS[compression]
|
|
331
|
+
compresslevel = compression_level if compression == 'deflated' else None
|
|
332
|
+
|
|
333
|
+
with zipfile.ZipFile(
|
|
334
|
+
archive,
|
|
335
|
+
mode='w',
|
|
336
|
+
compression=compression_method,
|
|
337
|
+
compresslevel=compresslevel,
|
|
338
|
+
) as zf:
|
|
339
|
+
for idx, rel_path in enumerate(file_list):
|
|
340
|
+
file_path = source / rel_path
|
|
341
|
+
if file_path.exists() and file_path.is_file():
|
|
342
|
+
zf.write(file_path, rel_path)
|
|
343
|
+
|
|
344
|
+
if progress_callback:
|
|
345
|
+
progress_callback(idx + 1, total_files)
|
|
346
|
+
|
|
347
|
+
return archive
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def extract_archive(
|
|
351
|
+
archive_path: str | Path,
|
|
352
|
+
output_path: str | Path,
|
|
353
|
+
*,
|
|
354
|
+
progress_callback: ProgressCallback | None = None,
|
|
355
|
+
) -> Path:
|
|
356
|
+
"""Extract a ZIP archive.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
archive_path: Path to ZIP file.
|
|
360
|
+
output_path: Directory to extract to.
|
|
361
|
+
progress_callback: Optional callback for progress updates.
|
|
362
|
+
|
|
363
|
+
Returns:
|
|
364
|
+
Path to extraction directory.
|
|
365
|
+
|
|
366
|
+
Raises:
|
|
367
|
+
FileNotFoundError: If archive does not exist.
|
|
368
|
+
zipfile.BadZipFile: If archive is invalid.
|
|
369
|
+
|
|
370
|
+
Example:
|
|
371
|
+
>>> output_dir = extract_archive('/path/to/archive.zip', '/tmp/extracted')
|
|
372
|
+
"""
|
|
373
|
+
archive = Path(archive_path).resolve()
|
|
374
|
+
output = Path(output_path).resolve()
|
|
375
|
+
|
|
376
|
+
if not archive.exists():
|
|
377
|
+
raise FileNotFoundError(f'Archive not found: {archive}')
|
|
378
|
+
|
|
379
|
+
output.mkdir(parents=True, exist_ok=True)
|
|
380
|
+
|
|
381
|
+
with zipfile.ZipFile(archive, 'r') as zf:
|
|
382
|
+
members = zf.namelist()
|
|
383
|
+
total = len(members)
|
|
384
|
+
|
|
385
|
+
for idx, member in enumerate(members):
|
|
386
|
+
zf.extract(member, output)
|
|
387
|
+
|
|
388
|
+
if progress_callback:
|
|
389
|
+
progress_callback(idx + 1, total)
|
|
390
|
+
|
|
391
|
+
return output
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def list_archive_contents(archive_path: str | Path) -> list[str]:
|
|
395
|
+
"""List files in archive without extracting.
|
|
396
|
+
|
|
397
|
+
Args:
|
|
398
|
+
archive_path: Path to ZIP file.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
List of file paths in archive.
|
|
402
|
+
|
|
403
|
+
Raises:
|
|
404
|
+
FileNotFoundError: If archive does not exist.
|
|
405
|
+
zipfile.BadZipFile: If archive is invalid.
|
|
406
|
+
|
|
407
|
+
Example:
|
|
408
|
+
>>> files = list_archive_contents('/path/to/archive.zip')
|
|
409
|
+
>>> print(files)
|
|
410
|
+
['src/main.py', 'src/utils.py', 'README.md']
|
|
411
|
+
"""
|
|
412
|
+
archive = Path(archive_path).resolve()
|
|
413
|
+
|
|
414
|
+
if not archive.exists():
|
|
415
|
+
raise FileNotFoundError(f'Archive not found: {archive}')
|
|
416
|
+
|
|
417
|
+
with zipfile.ZipFile(archive, 'r') as zf:
|
|
418
|
+
return zf.namelist()
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def get_archive_size(archive_path: str | Path) -> int:
|
|
422
|
+
"""Get the size of an archive file in bytes.
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
archive_path: Path to ZIP file.
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
Size in bytes.
|
|
429
|
+
|
|
430
|
+
Raises:
|
|
431
|
+
FileNotFoundError: If archive does not exist.
|
|
432
|
+
"""
|
|
433
|
+
archive = Path(archive_path).resolve()
|
|
434
|
+
|
|
435
|
+
if not archive.exists():
|
|
436
|
+
raise FileNotFoundError(f'Archive not found: {archive}')
|
|
437
|
+
|
|
438
|
+
return archive.stat().st_size
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
__all__ = [
|
|
442
|
+
'ProgressCallback',
|
|
443
|
+
'ArchiveFilter',
|
|
444
|
+
'create_archive',
|
|
445
|
+
'create_archive_from_git',
|
|
446
|
+
'extract_archive',
|
|
447
|
+
'list_archive_contents',
|
|
448
|
+
'get_archive_size',
|
|
449
|
+
]
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Checksum utilities for file integrity verification."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import IO, Any, Literal
|
|
8
|
+
|
|
9
|
+
HashAlgorithm = Literal['md5', 'sha1', 'sha256', 'sha512']
|
|
10
|
+
|
|
11
|
+
# Default chunk size for reading large files: 1MB
|
|
12
|
+
DEFAULT_CHUNK_SIZE = 1024 * 1024
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_hasher(algorithm: HashAlgorithm) -> Any:
|
|
16
|
+
"""Get a hashlib hasher for the specified algorithm."""
|
|
17
|
+
match algorithm:
|
|
18
|
+
case 'md5':
|
|
19
|
+
return hashlib.md5()
|
|
20
|
+
case 'sha1':
|
|
21
|
+
return hashlib.sha1()
|
|
22
|
+
case 'sha256':
|
|
23
|
+
return hashlib.sha256()
|
|
24
|
+
case 'sha512':
|
|
25
|
+
return hashlib.sha512()
|
|
26
|
+
case _:
|
|
27
|
+
raise ValueError(f'Unsupported hash algorithm: {algorithm}')
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def calculate_checksum(
|
|
31
|
+
file_path: str | Path,
|
|
32
|
+
*,
|
|
33
|
+
algorithm: HashAlgorithm = 'md5',
|
|
34
|
+
chunk_size: int = DEFAULT_CHUNK_SIZE,
|
|
35
|
+
prefix: str = '',
|
|
36
|
+
) -> str:
|
|
37
|
+
"""Calculate file checksum using specified algorithm.
|
|
38
|
+
|
|
39
|
+
Reads file in chunks for memory efficiency with large files.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
file_path: Path to file to hash.
|
|
43
|
+
algorithm: Hash algorithm ('md5', 'sha1', 'sha256', 'sha512').
|
|
44
|
+
chunk_size: Size of chunks to read (default 1MB).
|
|
45
|
+
prefix: Optional prefix to prepend to result.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Hex digest string, optionally with prefix.
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
FileNotFoundError: If file does not exist.
|
|
52
|
+
ValueError: If algorithm is not supported.
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
>>> checksum = calculate_checksum('/path/to/file.zip')
|
|
56
|
+
>>> checksum_with_prefix = calculate_checksum('/path/to/file.zip', prefix='dev-')
|
|
57
|
+
"""
|
|
58
|
+
path = Path(file_path)
|
|
59
|
+
if not path.exists():
|
|
60
|
+
raise FileNotFoundError(f'File not found: {path}')
|
|
61
|
+
|
|
62
|
+
hasher = _get_hasher(algorithm)
|
|
63
|
+
|
|
64
|
+
with path.open('rb') as f:
|
|
65
|
+
while chunk := f.read(chunk_size):
|
|
66
|
+
hasher.update(chunk)
|
|
67
|
+
|
|
68
|
+
digest = hasher.hexdigest()
|
|
69
|
+
return f'{prefix}{digest}' if prefix else digest
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def calculate_checksum_from_bytes(
|
|
73
|
+
data: bytes,
|
|
74
|
+
*,
|
|
75
|
+
algorithm: HashAlgorithm = 'md5',
|
|
76
|
+
) -> str:
|
|
77
|
+
"""Calculate checksum from bytes data.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
data: Bytes to hash.
|
|
81
|
+
algorithm: Hash algorithm.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Hex digest string.
|
|
85
|
+
|
|
86
|
+
Example:
|
|
87
|
+
>>> checksum = calculate_checksum_from_bytes(b'hello world')
|
|
88
|
+
"""
|
|
89
|
+
hasher = _get_hasher(algorithm)
|
|
90
|
+
hasher.update(data)
|
|
91
|
+
return hasher.hexdigest()
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def calculate_checksum_from_file_object(
|
|
95
|
+
file: IO[bytes],
|
|
96
|
+
*,
|
|
97
|
+
algorithm: HashAlgorithm = 'md5',
|
|
98
|
+
chunk_size: int = DEFAULT_CHUNK_SIZE,
|
|
99
|
+
) -> str:
|
|
100
|
+
"""Calculate checksum from file-like object.
|
|
101
|
+
|
|
102
|
+
Resets file pointer to beginning if the object supports seek().
|
|
103
|
+
Does not close the file after reading.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
file: File-like object opened in binary mode.
|
|
107
|
+
algorithm: Hash algorithm.
|
|
108
|
+
chunk_size: Size of chunks to read.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Hex digest string.
|
|
112
|
+
|
|
113
|
+
Example:
|
|
114
|
+
>>> with open('/path/to/file', 'rb') as f:
|
|
115
|
+
... checksum = calculate_checksum_from_file_object(f)
|
|
116
|
+
"""
|
|
117
|
+
hasher = _get_hasher(algorithm)
|
|
118
|
+
|
|
119
|
+
# Reset to beginning if possible
|
|
120
|
+
if hasattr(file, 'seek'):
|
|
121
|
+
file.seek(0)
|
|
122
|
+
|
|
123
|
+
while chunk := file.read(chunk_size):
|
|
124
|
+
hasher.update(chunk)
|
|
125
|
+
|
|
126
|
+
return hasher.hexdigest()
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def verify_checksum(
|
|
130
|
+
file_path: str | Path,
|
|
131
|
+
expected: str,
|
|
132
|
+
*,
|
|
133
|
+
algorithm: HashAlgorithm = 'md5',
|
|
134
|
+
) -> bool:
|
|
135
|
+
"""Verify file checksum matches expected value.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
file_path: Path to file.
|
|
139
|
+
expected: Expected checksum hex string (may include prefix).
|
|
140
|
+
algorithm: Hash algorithm used.
|
|
141
|
+
|
|
142
|
+
Returns:
|
|
143
|
+
True if checksum matches, False otherwise.
|
|
144
|
+
|
|
145
|
+
Example:
|
|
146
|
+
>>> is_valid = verify_checksum('/path/to/file.zip', 'abc123...')
|
|
147
|
+
"""
|
|
148
|
+
actual = calculate_checksum(file_path, algorithm=algorithm)
|
|
149
|
+
|
|
150
|
+
# Handle expected values with prefixes (e.g., 'dev-abc123...')
|
|
151
|
+
# Compare just the hex portion if expected has a prefix
|
|
152
|
+
if '-' in expected:
|
|
153
|
+
expected_hash = expected.split('-', 1)[-1]
|
|
154
|
+
else:
|
|
155
|
+
expected_hash = expected
|
|
156
|
+
|
|
157
|
+
return actual.lower() == expected_hash.lower()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
__all__ = [
|
|
161
|
+
'HashAlgorithm',
|
|
162
|
+
'DEFAULT_CHUNK_SIZE',
|
|
163
|
+
'calculate_checksum',
|
|
164
|
+
'calculate_checksum_from_bytes',
|
|
165
|
+
'calculate_checksum_from_file_object',
|
|
166
|
+
'verify_checksum',
|
|
167
|
+
]
|