synapse-sdk 2025.10.4__py3-none-any.whl → 2025.10.5__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/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-overview.md +25 -0
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/upload-plugins/upload-plugin-overview.md +25 -0
- synapse_sdk/plugins/categories/upload/actions/upload/action.py +8 -1
- synapse_sdk/plugins/categories/upload/actions/upload/context.py +0 -1
- synapse_sdk/plugins/categories/upload/actions/upload/steps/generate.py +6 -2
- synapse_sdk/plugins/categories/upload/actions/upload/steps/initialize.py +24 -9
- synapse_sdk/plugins/categories/upload/actions/upload/steps/metadata.py +27 -7
- synapse_sdk/plugins/categories/upload/actions/upload/steps/organize.py +45 -12
- synapse_sdk/plugins/categories/upload/actions/upload/steps/upload.py +10 -5
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/flat.py +31 -6
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/recursive.py +65 -37
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/default.py +17 -2
- {synapse_sdk-2025.10.4.dist-info → synapse_sdk-2025.10.5.dist-info}/METADATA +1 -1
- {synapse_sdk-2025.10.4.dist-info → synapse_sdk-2025.10.5.dist-info}/RECORD +18 -18
- {synapse_sdk-2025.10.4.dist-info → synapse_sdk-2025.10.5.dist-info}/WHEEL +0 -0
- {synapse_sdk-2025.10.4.dist-info → synapse_sdk-2025.10.5.dist-info}/entry_points.txt +0 -0
- {synapse_sdk-2025.10.4.dist-info → synapse_sdk-2025.10.5.dist-info}/licenses/LICENSE +0 -0
- {synapse_sdk-2025.10.4.dist-info → synapse_sdk-2025.10.5.dist-info}/top_level.txt +0 -0
|
@@ -97,6 +97,31 @@ Each asset has its own path and recursive setting. Perfect for distributed data
|
|
|
97
97
|
}
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
+
**Optional File Specs:**
|
|
101
|
+
|
|
102
|
+
In multi-path mode, file specifications can be marked as optional in the data collection's file specification template:
|
|
103
|
+
|
|
104
|
+
- **Required specs** (`is_required: true`): Must have an asset path in the `assets` parameter
|
|
105
|
+
- **Optional specs** (`is_required: false`): Can be omitted from `assets` - the system will skip them
|
|
106
|
+
|
|
107
|
+
Example with optional spec omitted:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"name": "Multi-Source Upload",
|
|
112
|
+
"use_single_path": false,
|
|
113
|
+
"assets": {
|
|
114
|
+
"pcd_1": {"path": "/sensors/lidar", "is_recursive": false},
|
|
115
|
+
"image_1": {"path": "/cameras/front", "is_recursive": true}
|
|
116
|
+
// "json_meta_1" is optional and omitted
|
|
117
|
+
},
|
|
118
|
+
"storage": 1,
|
|
119
|
+
"data_collection": 5
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
The system logs: `"Skipping optional spec json_meta_1: no asset path configured"`
|
|
124
|
+
|
|
100
125
|
## Basic Usage
|
|
101
126
|
|
|
102
127
|
### CLI Usage
|
|
@@ -97,6 +97,31 @@ sidebar_position: 1
|
|
|
97
97
|
}
|
|
98
98
|
```
|
|
99
99
|
|
|
100
|
+
**선택적 파일 사양:**
|
|
101
|
+
|
|
102
|
+
다중 경로 모드에서는 데이터 컬렉션의 파일 사양 템플릿에서 파일 사양을 선택적으로 표시할 수 있습니다:
|
|
103
|
+
|
|
104
|
+
- **필수 사양** (`is_required: true`): `assets` 매개변수에 자산 경로가 반드시 있어야 합니다
|
|
105
|
+
- **선택적 사양** (`is_required: false`): `assets`에서 생략 가능 - 시스템이 건너뜁니다
|
|
106
|
+
|
|
107
|
+
선택적 사양이 생략된 예제:
|
|
108
|
+
|
|
109
|
+
```json
|
|
110
|
+
{
|
|
111
|
+
"name": "다중 소스 업로드",
|
|
112
|
+
"use_single_path": false,
|
|
113
|
+
"assets": {
|
|
114
|
+
"pcd_1": {"path": "/sensors/lidar", "is_recursive": false},
|
|
115
|
+
"image_1": {"path": "/cameras/front", "is_recursive": true}
|
|
116
|
+
// "json_meta_1"은 선택사항이며 생략됨
|
|
117
|
+
},
|
|
118
|
+
"storage": 1,
|
|
119
|
+
"data_collection": 5
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
시스템 로그: `"Skipping optional spec json_meta_1: no asset path configured"`
|
|
124
|
+
|
|
100
125
|
## 기본 사용법
|
|
101
126
|
|
|
102
127
|
### CLI 사용법
|
|
@@ -173,11 +173,18 @@ class UploadAction(Action):
|
|
|
173
173
|
organized_files = context.get('organized_files', [])
|
|
174
174
|
file_specification_template = context.get('file_specification_template', {})
|
|
175
175
|
pathlib_cwd = context.get('pathlib_cwd')
|
|
176
|
+
use_single_path = context.get_param('use_single_path', True)
|
|
176
177
|
|
|
177
|
-
|
|
178
|
+
# Validate required data based on mode
|
|
179
|
+
if not organized_files or not file_specification_template:
|
|
178
180
|
raise ActionError('Required data not available from workflow steps')
|
|
179
181
|
|
|
182
|
+
# In single-path mode, pathlib_cwd is required
|
|
183
|
+
if use_single_path and not pathlib_cwd:
|
|
184
|
+
raise ActionError('pathlib_cwd is required in single-path mode')
|
|
185
|
+
|
|
180
186
|
# CRITICAL: Integrate with existing uploader mechanism
|
|
187
|
+
# In multi-path mode, pathlib_cwd may be None, but uploader should still work
|
|
181
188
|
uploader = self.get_uploader(pathlib_cwd, file_specification_template, organized_files, self.params)
|
|
182
189
|
organized_files = uploader.handle_upload_files()
|
|
183
190
|
|
|
@@ -31,7 +31,9 @@ class GenerateDataUnitsStep(BaseStep):
|
|
|
31
31
|
context.run.log_message_with_code(LogCode.GENERATING_DATA_UNITS)
|
|
32
32
|
|
|
33
33
|
# Initialize metrics
|
|
34
|
-
|
|
34
|
+
initial_metrics = {'stand_by': upload_result_count, 'success': 0, 'failed': 0}
|
|
35
|
+
context.update_metrics('data_units', initial_metrics)
|
|
36
|
+
context.run.set_metrics(initial_metrics, category='data_units')
|
|
35
37
|
|
|
36
38
|
# Get batch size from parameters
|
|
37
39
|
batch_size = context.get_param('creating_data_unit_batch_size', 1)
|
|
@@ -49,7 +51,9 @@ class GenerateDataUnitsStep(BaseStep):
|
|
|
49
51
|
)
|
|
50
52
|
|
|
51
53
|
# Update final metrics
|
|
52
|
-
|
|
54
|
+
final_metrics = {'stand_by': 0, 'success': len(generated_data_units), 'failed': 0}
|
|
55
|
+
context.update_metrics('data_units', final_metrics)
|
|
56
|
+
context.run.set_metrics(final_metrics, category='data_units')
|
|
53
57
|
|
|
54
58
|
# Complete progress
|
|
55
59
|
context.run.set_progress(upload_result_count, upload_result_count, category='generate_data_units')
|
|
@@ -29,19 +29,34 @@ class InitializeStep(BaseStep):
|
|
|
29
29
|
except Exception as e:
|
|
30
30
|
return self.create_error_result(f'Failed to get storage {storage_id}: {str(e)}')
|
|
31
31
|
|
|
32
|
-
#
|
|
32
|
+
# Check if we're in multi-path mode
|
|
33
|
+
use_single_path = context.get_param('use_single_path', True)
|
|
34
|
+
|
|
35
|
+
# Get and validate path (only required in single-path mode)
|
|
33
36
|
path = context.get_param('path')
|
|
34
|
-
|
|
35
|
-
return self.create_error_result('Path parameter is required')
|
|
37
|
+
pathlib_cwd = None
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
if use_single_path:
|
|
40
|
+
# Single-path mode: global path is required
|
|
41
|
+
if path is None:
|
|
42
|
+
return self.create_error_result('Path parameter is required in single-path mode')
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
pathlib_cwd = get_pathlib(storage, path)
|
|
46
|
+
context.set_pathlib_cwd(pathlib_cwd)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
return self.create_error_result(f'Failed to get path {path}: {str(e)}')
|
|
49
|
+
else:
|
|
50
|
+
# Multi-path mode: global path is optional (each asset has its own path)
|
|
51
|
+
if path:
|
|
52
|
+
try:
|
|
53
|
+
pathlib_cwd = get_pathlib(storage, path)
|
|
54
|
+
context.set_pathlib_cwd(pathlib_cwd)
|
|
55
|
+
except Exception as e:
|
|
56
|
+
return self.create_error_result(f'Failed to get path {path}: {str(e)}')
|
|
42
57
|
|
|
43
58
|
# Return success with rollback data
|
|
44
|
-
rollback_data = {'storage_id': storage_id, 'path': path}
|
|
59
|
+
rollback_data = {'storage_id': storage_id, 'path': path, 'use_single_path': use_single_path}
|
|
45
60
|
|
|
46
61
|
return self.create_success_result(
|
|
47
62
|
data={'storage': storage, 'pathlib_cwd': pathlib_cwd}, rollback_data=rollback_data
|
|
@@ -61,9 +61,15 @@ class ProcessMetadataStep(BaseStep):
|
|
|
61
61
|
excel_metadata = metadata_strategy.extract(excel_path)
|
|
62
62
|
else:
|
|
63
63
|
# Look for default metadata files (meta.xlsx, meta.xls)
|
|
64
|
-
|
|
65
|
-
if
|
|
66
|
-
|
|
64
|
+
# Only possible in single-path mode where pathlib_cwd is set
|
|
65
|
+
if context.pathlib_cwd:
|
|
66
|
+
excel_path = self._find_excel_metadata_file(context.pathlib_cwd)
|
|
67
|
+
if excel_path:
|
|
68
|
+
excel_metadata = metadata_strategy.extract(excel_path)
|
|
69
|
+
else:
|
|
70
|
+
context.run.log_message(
|
|
71
|
+
'No Excel metadata specified and multi-path mode - skipping metadata processing'
|
|
72
|
+
)
|
|
67
73
|
|
|
68
74
|
# Validate extracted metadata
|
|
69
75
|
if excel_metadata:
|
|
@@ -136,9 +142,13 @@ class ProcessMetadataStep(BaseStep):
|
|
|
136
142
|
if path.exists() and path.is_file():
|
|
137
143
|
return path, False
|
|
138
144
|
|
|
139
|
-
# Try relative to cwd
|
|
140
|
-
|
|
141
|
-
|
|
145
|
+
# Try relative to cwd (only if pathlib_cwd is set)
|
|
146
|
+
if context.pathlib_cwd:
|
|
147
|
+
path = context.pathlib_cwd / excel_path_str
|
|
148
|
+
return (path, False) if path.exists() else (None, False)
|
|
149
|
+
|
|
150
|
+
# In multi-path mode without pathlib_cwd, can only use absolute paths
|
|
151
|
+
return (None, False)
|
|
142
152
|
|
|
143
153
|
def _resolve_excel_path_from_base64(
|
|
144
154
|
self, excel_config: dict | ExcelMetadataFile, context: UploadContext
|
|
@@ -179,7 +189,17 @@ class ProcessMetadataStep(BaseStep):
|
|
|
179
189
|
return None, False
|
|
180
190
|
|
|
181
191
|
def _find_excel_metadata_file(self, pathlib_cwd: Path) -> Path:
|
|
182
|
-
"""Find default Excel metadata file.
|
|
192
|
+
"""Find default Excel metadata file.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
pathlib_cwd: Working directory path (must not be None)
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
Path to Excel metadata file, or None if not found
|
|
199
|
+
"""
|
|
200
|
+
if not pathlib_cwd:
|
|
201
|
+
return None
|
|
202
|
+
|
|
183
203
|
# Check .xlsx first as it's more common
|
|
184
204
|
excel_path = pathlib_cwd / 'meta.xlsx'
|
|
185
205
|
if excel_path.exists():
|
|
@@ -90,25 +90,40 @@ class OrganizeFilesStep(BaseStep):
|
|
|
90
90
|
if not assets:
|
|
91
91
|
return self.create_error_result('Multi-path mode requires assets configuration')
|
|
92
92
|
|
|
93
|
+
# Validate that all required specs have asset paths
|
|
94
|
+
required_specs = [spec['name'] for spec in context.file_specifications if spec.get('is_required', False)]
|
|
95
|
+
missing_required = [spec for spec in required_specs if spec not in assets]
|
|
96
|
+
|
|
97
|
+
if missing_required:
|
|
98
|
+
return self.create_error_result(
|
|
99
|
+
f'Multi-path mode requires asset paths for required specs: {", ".join(missing_required)}'
|
|
100
|
+
)
|
|
101
|
+
|
|
93
102
|
context.run.log_message(f'Using multi-path mode with {len(assets)} asset configurations')
|
|
94
103
|
context.run.log_message_with_code(LogCode.FILE_ORGANIZATION_STARTED)
|
|
95
104
|
|
|
96
|
-
|
|
105
|
+
# Collect all files and specs first
|
|
106
|
+
all_files = []
|
|
97
107
|
type_dirs = {}
|
|
108
|
+
specs_with_files = []
|
|
98
109
|
|
|
99
110
|
for spec in context.file_specifications:
|
|
100
111
|
spec_name = spec['name']
|
|
112
|
+
is_required = spec.get('is_required', False)
|
|
101
113
|
|
|
102
|
-
# Skip if no asset configuration for this spec
|
|
114
|
+
# Skip if no asset configuration for this spec (only allowed for optional specs)
|
|
103
115
|
if spec_name not in assets:
|
|
104
|
-
|
|
116
|
+
if is_required:
|
|
117
|
+
# This should not happen due to validation above, but double-check
|
|
118
|
+
return self.create_error_result(f'Required spec {spec_name} missing asset path')
|
|
119
|
+
context.run.log_message(f'Skipping optional spec {spec_name}: no asset path configured')
|
|
105
120
|
continue
|
|
106
121
|
|
|
107
122
|
asset_config = assets[spec_name]
|
|
108
123
|
|
|
109
124
|
# Get the asset path from storage
|
|
110
125
|
try:
|
|
111
|
-
asset_path = get_pathlib(context.storage, asset_config.path)
|
|
126
|
+
asset_path = get_pathlib(context.storage, asset_config.get('path', ''))
|
|
112
127
|
type_dirs[spec_name] = asset_path
|
|
113
128
|
except Exception as e:
|
|
114
129
|
context.run.log_message(f'Error accessing path for {spec_name}: {str(e)}', 'WARNING')
|
|
@@ -119,9 +134,9 @@ class OrganizeFilesStep(BaseStep):
|
|
|
119
134
|
continue
|
|
120
135
|
|
|
121
136
|
# Discover files for this asset
|
|
122
|
-
is_recursive = asset_config.is_recursive
|
|
137
|
+
is_recursive = asset_config.get('is_recursive', True)
|
|
123
138
|
context.run.log_message(
|
|
124
|
-
f'Discovering files for {spec_name} at {asset_config.path} (recursive={is_recursive})'
|
|
139
|
+
f'Discovering files for {spec_name} at {asset_config.get("path", "")} (recursive={is_recursive})'
|
|
125
140
|
)
|
|
126
141
|
|
|
127
142
|
files = file_discovery_strategy.discover(asset_path, is_recursive)
|
|
@@ -130,14 +145,23 @@ class OrganizeFilesStep(BaseStep):
|
|
|
130
145
|
context.run.log_message(f'No files found for {spec_name}', 'WARNING')
|
|
131
146
|
continue
|
|
132
147
|
|
|
133
|
-
|
|
134
|
-
|
|
148
|
+
all_files.extend(files)
|
|
149
|
+
specs_with_files.append(spec)
|
|
150
|
+
context.run.log_message(f'Found {len(files)} files for {spec_name}')
|
|
135
151
|
|
|
136
|
-
|
|
137
|
-
|
|
152
|
+
# Organize all files together to group by dataset_key
|
|
153
|
+
all_organized_files = []
|
|
154
|
+
if all_files and specs_with_files:
|
|
155
|
+
context.run.log_message(f'Organizing {len(all_files)} files across {len(specs_with_files)} specs')
|
|
156
|
+
context.run.log_message(f'Type directories: {list(type_dirs.keys())}')
|
|
157
|
+
|
|
158
|
+
all_organized_files = file_discovery_strategy.organize(
|
|
159
|
+
all_files, specs_with_files, context.metadata or {}, type_dirs
|
|
160
|
+
)
|
|
138
161
|
|
|
139
162
|
if all_organized_files:
|
|
140
163
|
context.run.log_message_with_code(LogCode.FILES_DISCOVERED, len(all_organized_files))
|
|
164
|
+
context.run.log_message(f'Created {len(all_organized_files)} data units from {len(all_files)} files')
|
|
141
165
|
context.add_organized_files(all_organized_files)
|
|
142
166
|
else:
|
|
143
167
|
context.run.log_message_with_code(LogCode.NO_FILES_FOUND_WARNING)
|
|
@@ -159,8 +183,17 @@ class OrganizeFilesStep(BaseStep):
|
|
|
159
183
|
|
|
160
184
|
def validate_prerequisites(self, context: UploadContext) -> None:
|
|
161
185
|
"""Validate prerequisites for file organization."""
|
|
162
|
-
|
|
163
|
-
|
|
186
|
+
use_single_path = context.get_param('use_single_path', True)
|
|
187
|
+
|
|
188
|
+
# In single-path mode, pathlib_cwd is required
|
|
189
|
+
if use_single_path and not context.pathlib_cwd:
|
|
190
|
+
raise ValueError('Working directory path not set in single-path mode')
|
|
191
|
+
|
|
192
|
+
# In multi-path mode, pathlib_cwd is optional (each asset has its own path)
|
|
193
|
+
if not use_single_path:
|
|
194
|
+
assets = context.get_param('assets', {})
|
|
195
|
+
if not assets:
|
|
196
|
+
raise ValueError('Multi-path mode requires assets configuration')
|
|
164
197
|
|
|
165
198
|
if not context.file_specifications:
|
|
166
199
|
raise ValueError('File specifications not available')
|
|
@@ -34,7 +34,9 @@ class UploadFilesStep(BaseStep):
|
|
|
34
34
|
context.run.log_message_with_code(LogCode.UPLOADING_DATA_FILES)
|
|
35
35
|
|
|
36
36
|
# Initialize metrics
|
|
37
|
-
|
|
37
|
+
initial_metrics = {'stand_by': organized_files_count, 'success': 0, 'failed': 0}
|
|
38
|
+
context.update_metrics('data_files', initial_metrics)
|
|
39
|
+
context.run.set_metrics(initial_metrics, category='data_files')
|
|
38
40
|
|
|
39
41
|
# Create upload configuration
|
|
40
42
|
upload_config = UploadConfig(
|
|
@@ -55,10 +57,13 @@ class UploadFilesStep(BaseStep):
|
|
|
55
57
|
context.run.log_data_file(uploaded_file, UploadStatus.SUCCESS)
|
|
56
58
|
|
|
57
59
|
# Update final metrics
|
|
58
|
-
|
|
59
|
-
'
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
final_metrics = {
|
|
61
|
+
'stand_by': 0,
|
|
62
|
+
'success': len(uploaded_files),
|
|
63
|
+
'failed': organized_files_count - len(uploaded_files),
|
|
64
|
+
}
|
|
65
|
+
context.update_metrics('data_files', final_metrics)
|
|
66
|
+
context.run.set_metrics(final_metrics, category='data_files')
|
|
62
67
|
|
|
63
68
|
# Complete progress
|
|
64
69
|
context.run.set_progress(organized_files_count, organized_files_count, category='upload_data_files')
|
|
@@ -10,7 +10,11 @@ class FlatFileDiscoveryStrategy(FileDiscoveryStrategy):
|
|
|
10
10
|
|
|
11
11
|
def discover(self, path: Path, recursive: bool) -> List[Path]:
|
|
12
12
|
"""Discover files non-recursively in the given path."""
|
|
13
|
-
|
|
13
|
+
# Exclude system files
|
|
14
|
+
excluded_files = {'.DS_Store', 'Thumbs.db', 'desktop.ini'}
|
|
15
|
+
return [
|
|
16
|
+
file_path for file_path in path.glob('*') if file_path.is_file() and file_path.name not in excluded_files
|
|
17
|
+
]
|
|
14
18
|
|
|
15
19
|
def organize(self, files: List[Path], specs: Dict, metadata: Dict, type_dirs: Dict = None) -> List[Dict]:
|
|
16
20
|
"""Organize files according to specifications with metadata."""
|
|
@@ -39,9 +43,14 @@ class FlatFileDiscoveryStrategy(FileDiscoveryStrategy):
|
|
|
39
43
|
# Performance optimization 2: Build metadata index for faster lookups
|
|
40
44
|
metadata_index = self._build_metadata_index(metadata)
|
|
41
45
|
|
|
42
|
-
# Group files by
|
|
46
|
+
# Group files by dataset_key (stem-based matching) - flat discovery (no subdirectories)
|
|
47
|
+
# Strategy:
|
|
48
|
+
# 1. Group all files (required + optional) by their file stem
|
|
49
|
+
# 2. Only create data units for groups that have ALL required files
|
|
50
|
+
# 3. Optional files are automatically included if they match the stem
|
|
43
51
|
dataset_files = {}
|
|
44
52
|
required_specs = [spec['name'] for spec in specs if spec.get('is_required', False)]
|
|
53
|
+
optional_specs = [spec['name'] for spec in specs if not spec.get('is_required', False)]
|
|
45
54
|
|
|
46
55
|
for file_path in files:
|
|
47
56
|
# Determine which type directory this file belongs to
|
|
@@ -64,20 +73,36 @@ class FlatFileDiscoveryStrategy(FileDiscoveryStrategy):
|
|
|
64
73
|
# If stat fails, keep existing file
|
|
65
74
|
pass
|
|
66
75
|
|
|
67
|
-
# Create organized files for datasets with
|
|
76
|
+
# Create organized files ONLY for datasets with ALL required files
|
|
77
|
+
# Optional files are included automatically if they match the stem
|
|
68
78
|
for file_name, files_dict in sorted(dataset_files.items()):
|
|
69
|
-
if all
|
|
70
|
-
|
|
79
|
+
# Check if all required files are present
|
|
80
|
+
has_all_required = all(req in files_dict for req in required_specs)
|
|
81
|
+
|
|
82
|
+
if has_all_required:
|
|
83
|
+
# Extract original file stem from actual file paths (more reliable)
|
|
84
|
+
# Collect stems from all files in the group
|
|
85
|
+
file_stems = {}
|
|
71
86
|
file_extensions = {}
|
|
87
|
+
|
|
72
88
|
for file_path in files_dict.values():
|
|
89
|
+
stem = file_path.stem
|
|
73
90
|
ext = file_path.suffix.lower()
|
|
91
|
+
|
|
92
|
+
# Count stems (to handle multiple files with slightly different names)
|
|
93
|
+
if stem:
|
|
94
|
+
file_stems[stem] = file_stems.get(stem, 0) + 1
|
|
95
|
+
|
|
96
|
+
# Count extensions
|
|
74
97
|
if ext:
|
|
75
98
|
file_extensions[ext] = file_extensions.get(ext, 0) + 1
|
|
76
99
|
|
|
100
|
+
# Use the most common stem (usually they're all the same)
|
|
101
|
+
original_stem = max(file_stems, key=file_stems.get) if file_stems else file_name
|
|
77
102
|
origin_file_extension = max(file_extensions, key=file_extensions.get) if file_extensions else ''
|
|
78
103
|
|
|
79
104
|
meta_data = {
|
|
80
|
-
'origin_file_stem':
|
|
105
|
+
'origin_file_stem': original_stem,
|
|
81
106
|
'origin_file_extension': origin_file_extension,
|
|
82
107
|
'created_at': datetime.now().isoformat(),
|
|
83
108
|
}
|
|
@@ -10,7 +10,14 @@ class RecursiveFileDiscoveryStrategy(FileDiscoveryStrategy):
|
|
|
10
10
|
|
|
11
11
|
def discover(self, path: Path, recursive: bool) -> List[Path]:
|
|
12
12
|
"""Discover files recursively in the given path."""
|
|
13
|
-
|
|
13
|
+
# Exclude system directories
|
|
14
|
+
excluded_dirs = {'@eaDir', '.@__thumb', '@Recycle', '#recycle', '.DS_Store', 'Thumbs.db', '.synology'}
|
|
15
|
+
|
|
16
|
+
def exclude_dirs(file_path: Path) -> bool:
|
|
17
|
+
"""Check if file path contains excluded directories."""
|
|
18
|
+
return any(excluded_dir in file_path.parts for excluded_dir in excluded_dirs)
|
|
19
|
+
|
|
20
|
+
return [file_path for file_path in path.rglob('*') if file_path.is_file() and not exclude_dirs(file_path)]
|
|
14
21
|
|
|
15
22
|
def organize(self, files: List[Path], specs: Dict, metadata: Dict, type_dirs: Dict = None) -> List[Dict]:
|
|
16
23
|
"""Organize files according to specifications with metadata."""
|
|
@@ -42,57 +49,78 @@ class RecursiveFileDiscoveryStrategy(FileDiscoveryStrategy):
|
|
|
42
49
|
# Performance optimization 2: Build metadata index for faster lookups
|
|
43
50
|
metadata_index = self._build_metadata_index(metadata)
|
|
44
51
|
|
|
45
|
-
# Group files by
|
|
52
|
+
# Group files by dataset_key (stem-based matching)
|
|
53
|
+
# Strategy:
|
|
54
|
+
# 1. Group all files (required + optional) by their file stem
|
|
55
|
+
# 2. Only create data units for groups that have ALL required files
|
|
56
|
+
# 3. Optional files are automatically included if they match the stem
|
|
46
57
|
dataset_files = {}
|
|
47
58
|
required_specs = [spec['name'] for spec in specs if spec.get('is_required', False)]
|
|
48
59
|
|
|
49
60
|
for file_path in files:
|
|
50
61
|
# Determine which type directory this file belongs to
|
|
51
|
-
|
|
62
|
+
matched = False
|
|
52
63
|
for spec_name, dir_path in type_dirs.items():
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
64
|
+
# Check if file is under this spec's directory
|
|
65
|
+
# Use try/except for relative_to to ensure proper path matching
|
|
66
|
+
try:
|
|
56
67
|
relative_path = file_path.relative_to(dir_path)
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
except (OSError, IOError):
|
|
75
|
-
# If stat fails, keep existing file
|
|
76
|
-
pass
|
|
77
|
-
|
|
78
|
-
# Create organized files for datasets with all required files
|
|
79
|
-
for dataset_key, files_dict in sorted(dataset_files.items()):
|
|
80
|
-
if all(req in files_dict for req in required_specs):
|
|
81
|
-
# Extract original file stem from dataset_key
|
|
82
|
-
# If dataset_key contains path info (parent_stem), extract just the stem part
|
|
83
|
-
if '_' in dataset_key and len(dataset_key.split('_')) >= 2:
|
|
84
|
-
# Extract the last part after the last underscore as the original stem
|
|
85
|
-
original_stem = dataset_key.split('_')[-1]
|
|
68
|
+
matched = True
|
|
69
|
+
except ValueError:
|
|
70
|
+
# File is not under this directory
|
|
71
|
+
continue
|
|
72
|
+
|
|
73
|
+
# Create unique dataset key using relative path from spec directory
|
|
74
|
+
# Use parent directory + stem as unique key to group related files
|
|
75
|
+
if relative_path.parent != Path('.'):
|
|
76
|
+
dataset_key = f'{relative_path.parent}_{file_path.stem}'
|
|
77
|
+
else:
|
|
78
|
+
dataset_key = file_path.stem
|
|
79
|
+
|
|
80
|
+
if dataset_key not in dataset_files:
|
|
81
|
+
dataset_files[dataset_key] = {}
|
|
82
|
+
|
|
83
|
+
if spec_name not in dataset_files[dataset_key]:
|
|
84
|
+
dataset_files[dataset_key][spec_name] = file_path
|
|
86
85
|
else:
|
|
87
|
-
|
|
86
|
+
# Keep the most recent file - only stat when needed
|
|
87
|
+
existing_file = dataset_files[dataset_key][spec_name]
|
|
88
|
+
try:
|
|
89
|
+
if file_path.stat().st_mtime > existing_file.stat().st_mtime:
|
|
90
|
+
dataset_files[dataset_key][spec_name] = file_path
|
|
91
|
+
except (OSError, IOError):
|
|
92
|
+
# If stat fails, keep existing file
|
|
93
|
+
pass
|
|
94
|
+
|
|
95
|
+
# Found matching directory, move to next file
|
|
96
|
+
break
|
|
97
|
+
|
|
98
|
+
# Create organized files ONLY for datasets with ALL required files
|
|
99
|
+
# Optional files are included automatically if they match the stem
|
|
100
|
+
for dataset_key, files_dict in sorted(dataset_files.items()):
|
|
101
|
+
# Check if all required files are present
|
|
102
|
+
has_all_required = all(req in files_dict for req in required_specs)
|
|
88
103
|
|
|
89
|
-
|
|
104
|
+
if has_all_required:
|
|
105
|
+
# Extract original file stem from actual file paths (more reliable than parsing dataset_key)
|
|
106
|
+
# Collect stems from all files in the group
|
|
107
|
+
file_stems = {}
|
|
90
108
|
file_extensions = {}
|
|
109
|
+
|
|
91
110
|
for file_path in files_dict.values():
|
|
111
|
+
stem = file_path.stem
|
|
92
112
|
ext = file_path.suffix.lower()
|
|
113
|
+
|
|
114
|
+
# Count stems (to handle multiple files with slightly different names)
|
|
115
|
+
if stem:
|
|
116
|
+
file_stems[stem] = file_stems.get(stem, 0) + 1
|
|
117
|
+
|
|
118
|
+
# Count extensions
|
|
93
119
|
if ext:
|
|
94
120
|
file_extensions[ext] = file_extensions.get(ext, 0) + 1
|
|
95
121
|
|
|
122
|
+
# Use the most common stem (usually they're all the same)
|
|
123
|
+
original_stem = max(file_stems, key=file_stems.get) if file_stems else dataset_key
|
|
96
124
|
origin_file_extension = max(file_extensions, key=file_extensions.get) if file_extensions else ''
|
|
97
125
|
|
|
98
126
|
meta_data = {
|
|
@@ -12,12 +12,24 @@ class DefaultValidationStrategy(ValidationStrategy):
|
|
|
12
12
|
"""Validate action parameters."""
|
|
13
13
|
errors = []
|
|
14
14
|
|
|
15
|
-
# Check required parameters
|
|
16
|
-
required_params = ['storage', 'data_collection', '
|
|
15
|
+
# Check required parameters (common to all modes)
|
|
16
|
+
required_params = ['storage', 'data_collection', 'name']
|
|
17
17
|
for param in required_params:
|
|
18
18
|
if param not in params:
|
|
19
19
|
errors.append(f'Missing required parameter: {param}')
|
|
20
20
|
|
|
21
|
+
# Check mode-specific requirements
|
|
22
|
+
use_single_path = params.get('use_single_path', True)
|
|
23
|
+
|
|
24
|
+
if use_single_path:
|
|
25
|
+
# Single-path mode: 'path' is required
|
|
26
|
+
if 'path' not in params:
|
|
27
|
+
errors.append("Missing required parameter 'path' in single-path mode")
|
|
28
|
+
else:
|
|
29
|
+
# Multi-path mode: 'assets' is required
|
|
30
|
+
if 'assets' not in params:
|
|
31
|
+
errors.append("Missing required parameter 'assets' in multi-path mode")
|
|
32
|
+
|
|
21
33
|
# Check parameter types
|
|
22
34
|
if 'storage' in params and not isinstance(params['storage'], int):
|
|
23
35
|
errors.append("Parameter 'storage' must be an integer")
|
|
@@ -28,6 +40,9 @@ class DefaultValidationStrategy(ValidationStrategy):
|
|
|
28
40
|
if 'is_recursive' in params and not isinstance(params['is_recursive'], bool):
|
|
29
41
|
errors.append("Parameter 'is_recursive' must be a boolean")
|
|
30
42
|
|
|
43
|
+
if 'use_single_path' in params and not isinstance(params['use_single_path'], bool):
|
|
44
|
+
errors.append("Parameter 'use_single_path' must be a boolean")
|
|
45
|
+
|
|
31
46
|
return ValidationResult(valid=len(errors) == 0, errors=errors)
|
|
32
47
|
|
|
33
48
|
def validate_files(self, files: List[Dict], specs: Dict) -> ValidationResult:
|
|
@@ -101,7 +101,7 @@ synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task
|
|
|
101
101
|
synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task-overview.md,sha256=J-z2KkLt5WWTgbJGRxvqrZQxfHvur_QtCGA6iT5a0Og,17134
|
|
102
102
|
synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task-template-development.md,sha256=ARirm2jq5X2fVXWpZFX6ezh0aSIK5PCWvYC9nvsXIwY,41159
|
|
103
103
|
synapse_sdk/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-action.md,sha256=GmTZYLyPDuUC9WzjNjf_zNh5FkN-VYzLuSo_dQzewWw,30796
|
|
104
|
-
synapse_sdk/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-overview.md,sha256=
|
|
104
|
+
synapse_sdk/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-overview.md,sha256=Q4jqN_W3AB-7PEDpkCxwKjmrjERGpb-UNqYA9I0O63Y,16513
|
|
105
105
|
synapse_sdk/devtools/docs/docs/plugins/categories/upload-plugins/upload-plugin-template.md,sha256=HpqGQ9yUYy4673gkdPNsqFZRdAXnDv3xbrC87brG50o,22288
|
|
106
106
|
synapse_sdk/devtools/docs/docs/tutorial-basics/_category_.json,sha256=qQVHZ1p0CxHg5Gb4CYNxUSeqZ3LVo4nXN_N0yUMVpDM,180
|
|
107
107
|
synapse_sdk/devtools/docs/docs/tutorial-basics/congratulations.md,sha256=zJbcwKNVYGpLAGJO7e3cTdptFhb6m_NUzYDU5i6MWgc,1078
|
|
@@ -154,7 +154,7 @@ synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins
|
|
|
154
154
|
synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/to-task-overview.md,sha256=InL4EI0BkVXgjs-j7cPRYaRXDVxxMgBYYLs_9FdyKxk,18408
|
|
155
155
|
synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/to-task-template-development.md,sha256=Ab8EeSYMWZclb_qxvBaLaz4IdCXySrKN2o68s24VT6I,42441
|
|
156
156
|
synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/upload-plugins/upload-plugin-action.md,sha256=zaodG2yGwNQiPLrI59dhShB0WxZ1EMNdXnQx9E5Kzlk,32523
|
|
157
|
-
synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/upload-plugins/upload-plugin-overview.md,sha256=
|
|
157
|
+
synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/upload-plugins/upload-plugin-overview.md,sha256=7xo-N4NwDvF3QFVKafZosjG1IbtwI-BzTTTyo4kzU44,17953
|
|
158
158
|
synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/upload-plugins/upload-plugin-template.md,sha256=Rldxjo5DKmHfEBMoF86yLkgYGoip99gVkF7gBSkqNxM,23603
|
|
159
159
|
synapse_sdk/devtools/docs/i18n/ko/docusaurus-theme-classic/footer.json,sha256=SCLmysIhJ6XEEfnL7cOQb3s08EG4LA-ColdRRxecIH8,1601
|
|
160
160
|
synapse_sdk/devtools/docs/i18n/ko/docusaurus-theme-classic/navbar.json,sha256=M-ly81cWnesYYtCRcfMvOsKl5P0z52K9kiw0T6GIQ60,429
|
|
@@ -274,8 +274,8 @@ synapse_sdk/plugins/categories/smart_tool/templates/plugin/auto_label.py,sha256=
|
|
|
274
274
|
synapse_sdk/plugins/categories/upload/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
275
275
|
synapse_sdk/plugins/categories/upload/actions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
276
276
|
synapse_sdk/plugins/categories/upload/actions/upload/__init__.py,sha256=ShmALSr3rncCMHYlSJkYwdvEOsEHUxAzG5-hoERhmI0,548
|
|
277
|
-
synapse_sdk/plugins/categories/upload/actions/upload/action.py,sha256=
|
|
278
|
-
synapse_sdk/plugins/categories/upload/actions/upload/context.py,sha256=
|
|
277
|
+
synapse_sdk/plugins/categories/upload/actions/upload/action.py,sha256=RtOV-Q2oG5oLggJQ5A2FKFj7axmIJT8wUcaF0zJTTZo,10357
|
|
278
|
+
synapse_sdk/plugins/categories/upload/actions/upload/context.py,sha256=4sBWs-IRstzK5U-9N-Ag1vuux6zos_D1P8MFQMaPrKk,6292
|
|
279
279
|
synapse_sdk/plugins/categories/upload/actions/upload/enums.py,sha256=TQ-HiGI_-15rc11XQiCT6O5hRzmjkdCnRhX3Ci3bKk0,7856
|
|
280
280
|
synapse_sdk/plugins/categories/upload/actions/upload/exceptions.py,sha256=QIpts9h78OD8GWCIGjX9jSV4v3Gb0cqH9Eo-6OwN1Qk,1223
|
|
281
281
|
synapse_sdk/plugins/categories/upload/actions/upload/factory.py,sha256=n8PE_LEz8ajLWmRg-CacBt0Bo0vhpDp89_QhvY2VMnQ,6336
|
|
@@ -288,11 +288,11 @@ synapse_sdk/plugins/categories/upload/actions/upload/steps/__init__.py,sha256=-O
|
|
|
288
288
|
synapse_sdk/plugins/categories/upload/actions/upload/steps/base.py,sha256=yzOmjQKlcsmlB5XZqGgQbCyNwwUiKa9nWd2-Pn22Vls,3682
|
|
289
289
|
synapse_sdk/plugins/categories/upload/actions/upload/steps/cleanup.py,sha256=JAc1L9UfiEhNQ2NWnBMX2Bs4Vej08QPYML6Mb0-a0jg,2200
|
|
290
290
|
synapse_sdk/plugins/categories/upload/actions/upload/steps/collection.py,sha256=zLm4BOuu4d8YqXOjOEDhbEUFQ2t4eSsPJB5e_FKYczI,2345
|
|
291
|
-
synapse_sdk/plugins/categories/upload/actions/upload/steps/generate.py,sha256=
|
|
292
|
-
synapse_sdk/plugins/categories/upload/actions/upload/steps/initialize.py,sha256=
|
|
293
|
-
synapse_sdk/plugins/categories/upload/actions/upload/steps/metadata.py,sha256=
|
|
294
|
-
synapse_sdk/plugins/categories/upload/actions/upload/steps/organize.py,sha256=
|
|
295
|
-
synapse_sdk/plugins/categories/upload/actions/upload/steps/upload.py,sha256=
|
|
291
|
+
synapse_sdk/plugins/categories/upload/actions/upload/steps/generate.py,sha256=dekCGjg-uSTBY7eNcM7emptl9Yxr038xup86Kfea-NE,3604
|
|
292
|
+
synapse_sdk/plugins/categories/upload/actions/upload/steps/initialize.py,sha256=JaPK1Mn7Cq4z69AIYps6iOsvVztownW5RnvyhbKhiGk,3137
|
|
293
|
+
synapse_sdk/plugins/categories/upload/actions/upload/steps/metadata.py,sha256=bQ9V8BaZ0LW42_cCsojA3hyQoa7RUCUGOzEaNCqN9wM,9197
|
|
294
|
+
synapse_sdk/plugins/categories/upload/actions/upload/steps/organize.py,sha256=0MqkqeFxDjVFt5Jn_eOwHCrjhUNJU6xnS8H71eGkPdE,8712
|
|
295
|
+
synapse_sdk/plugins/categories/upload/actions/upload/steps/upload.py,sha256=GPw5RyTZLAPY2uPeay1iTMf0U86lTDT3n99FdRsBzgA,4147
|
|
296
296
|
synapse_sdk/plugins/categories/upload/actions/upload/steps/validate.py,sha256=31N7lb41G1-LlwLaGkCQkKB8ep4FPxxtLhp0sv3-1Vs,2427
|
|
297
297
|
synapse_sdk/plugins/categories/upload/actions/upload/strategies/__init__.py,sha256=JXysLlVGJDVtqZYFE6WIGcKgFNHcjqTzC0HO233Sak0,54
|
|
298
298
|
synapse_sdk/plugins/categories/upload/actions/upload/strategies/base.py,sha256=fMzTpZA6M-qqi9ZHBQ134a5w97QChiafHoi03AqwTpA,2488
|
|
@@ -300,8 +300,8 @@ synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/__init
|
|
|
300
300
|
synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/batch.py,sha256=YzKq5wHvrQ1_Tv__1xsX1ArLOEwgJNkDilZHg9k7GTY,1475
|
|
301
301
|
synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/single.py,sha256=bjvWu5t_nAK9Z8AWM_n1uwusMWd9G-7ddqSmwsH39l0,1344
|
|
302
302
|
synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/__init__.py,sha256=ELVncVuhTFvPrbGUjZZAD7upvV3IEsaRi9eTXyjaOU8,42
|
|
303
|
-
synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/flat.py,sha256=
|
|
304
|
-
synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/recursive.py,sha256=
|
|
303
|
+
synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/flat.py,sha256=QLd8pS9y3NyeNRQf1Ch5xFkwGRFjJoYXgV6QmHAn8hs,11155
|
|
304
|
+
synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/recursive.py,sha256=Q2tvBmuhlNndBVHR9asBaRH_N2Sx7N4doCXT1uLkP-M,12243
|
|
305
305
|
synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/__init__.py,sha256=d3sSL9boHCmYmbWupbJ7s8LOBakGCgjiPKRQIbfDCs8,36
|
|
306
306
|
synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/excel.py,sha256=LlykEvwfyuRRCWozExQoolvIfHH1-B4vHOu7dykSTnQ,7333
|
|
307
307
|
synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/none.py,sha256=RGcSmc4N9JnqxC8R-AEo_4_wmg1KS0lij1PxXij9ipk,507
|
|
@@ -309,7 +309,7 @@ synapse_sdk/plugins/categories/upload/actions/upload/strategies/upload/__init__.
|
|
|
309
309
|
synapse_sdk/plugins/categories/upload/actions/upload/strategies/upload/async_upload.py,sha256=Jn-yK0BqLgx1zRmIh9AJCDPsM2B9Yzu_0im5U305vnw,5720
|
|
310
310
|
synapse_sdk/plugins/categories/upload/actions/upload/strategies/upload/sync.py,sha256=IkRpy561HMHE1Ji0l2Nv21CfH9ZiCrm4qBCADeZFTXs,1875
|
|
311
311
|
synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/__init__.py,sha256=aq-sI8tScvc0rX21qlLeBgJyKaVpC0hziMB5vcg5K3Y,38
|
|
312
|
-
synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/default.py,sha256=
|
|
312
|
+
synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/default.py,sha256=3_jLBqw2snnBAtDuKKwVS6ImMwDZHeOSnqph_jCq2nE,2515
|
|
313
313
|
synapse_sdk/plugins/categories/upload/templates/README.md,sha256=30BC3CpHrPmjPeLExFtjnYi6qZrBcDdn4fOc5F-uMmc,8995
|
|
314
314
|
synapse_sdk/plugins/categories/upload/templates/config.yaml,sha256=xrLMhK6QTvxnqep7ulrEuq8DxVPS7TOI2cogCaBxK2g,1127
|
|
315
315
|
synapse_sdk/plugins/categories/upload/templates/plugin/__init__.py,sha256=G8xv24PGWmL84Y77tElaEJhbjyjxklkqqdmxmZ834L0,11239
|
|
@@ -376,9 +376,9 @@ synapse_sdk/utils/storage/providers/gcp.py,sha256=i2BQCu1Kej1If9SuNr2_lEyTcr5M_n
|
|
|
376
376
|
synapse_sdk/utils/storage/providers/http.py,sha256=2DhIulND47JOnS5ZY7MZUex7Su3peAPksGo1Wwg07L4,5828
|
|
377
377
|
synapse_sdk/utils/storage/providers/s3.py,sha256=ZmqekAvIgcQBdRU-QVJYv1Rlp6VHfXwtbtjTSphua94,2573
|
|
378
378
|
synapse_sdk/utils/storage/providers/sftp.py,sha256=_8s9hf0JXIO21gvm-JVS00FbLsbtvly4c-ETLRax68A,1426
|
|
379
|
-
synapse_sdk-2025.10.
|
|
380
|
-
synapse_sdk-2025.10.
|
|
381
|
-
synapse_sdk-2025.10.
|
|
382
|
-
synapse_sdk-2025.10.
|
|
383
|
-
synapse_sdk-2025.10.
|
|
384
|
-
synapse_sdk-2025.10.
|
|
379
|
+
synapse_sdk-2025.10.5.dist-info/licenses/LICENSE,sha256=bKzmC5YAg4V1Fhl8OO_tqY8j62hgdncAkN7VrdjmrGk,1101
|
|
380
|
+
synapse_sdk-2025.10.5.dist-info/METADATA,sha256=N1gJlSwg8kEhRTkXxmHMAJntzig0DtxmmV_t1Z5uPkE,4186
|
|
381
|
+
synapse_sdk-2025.10.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
382
|
+
synapse_sdk-2025.10.5.dist-info/entry_points.txt,sha256=VNptJoGoNJI8yLXfBmhgUefMsmGI0m3-0YoMvrOgbxo,48
|
|
383
|
+
synapse_sdk-2025.10.5.dist-info/top_level.txt,sha256=ytgJMRK1slVOKUpgcw3LEyHHP7S34J6n_gJzdkcSsw8,12
|
|
384
|
+
synapse_sdk-2025.10.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|