shotgun-sh 0.2.8.dev2__py3-none-any.whl → 0.3.3.dev1__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.
- shotgun/agents/agent_manager.py +382 -60
- shotgun/agents/common.py +15 -9
- shotgun/agents/config/README.md +89 -0
- shotgun/agents/config/__init__.py +10 -1
- shotgun/agents/config/constants.py +0 -6
- shotgun/agents/config/manager.py +383 -82
- shotgun/agents/config/models.py +122 -18
- shotgun/agents/config/provider.py +81 -15
- shotgun/agents/config/streaming_test.py +119 -0
- shotgun/agents/context_analyzer/__init__.py +28 -0
- shotgun/agents/context_analyzer/analyzer.py +475 -0
- shotgun/agents/context_analyzer/constants.py +9 -0
- shotgun/agents/context_analyzer/formatter.py +115 -0
- shotgun/agents/context_analyzer/models.py +212 -0
- shotgun/agents/conversation/__init__.py +18 -0
- shotgun/agents/conversation/filters.py +164 -0
- shotgun/agents/conversation/history/chunking.py +278 -0
- shotgun/agents/{history → conversation/history}/compaction.py +36 -5
- shotgun/agents/{history → conversation/history}/constants.py +5 -0
- shotgun/agents/conversation/history/file_content_deduplication.py +216 -0
- shotgun/agents/{history → conversation/history}/history_processors.py +380 -8
- shotgun/agents/{history → conversation/history}/token_counting/anthropic.py +25 -1
- shotgun/agents/{history → conversation/history}/token_counting/base.py +14 -3
- shotgun/agents/{history → conversation/history}/token_counting/openai.py +11 -1
- shotgun/agents/{history → conversation/history}/token_counting/sentencepiece_counter.py +8 -0
- shotgun/agents/{history → conversation/history}/token_counting/tokenizer_cache.py +3 -1
- shotgun/agents/{history → conversation/history}/token_counting/utils.py +0 -3
- shotgun/agents/{conversation_manager.py → conversation/manager.py} +36 -20
- shotgun/agents/{conversation_history.py → conversation/models.py} +8 -92
- shotgun/agents/error/__init__.py +11 -0
- shotgun/agents/error/models.py +19 -0
- shotgun/agents/export.py +2 -2
- shotgun/agents/plan.py +2 -2
- shotgun/agents/research.py +3 -3
- shotgun/agents/runner.py +230 -0
- shotgun/agents/specify.py +2 -2
- shotgun/agents/tasks.py +2 -2
- shotgun/agents/tools/codebase/codebase_shell.py +6 -0
- shotgun/agents/tools/codebase/directory_lister.py +6 -0
- shotgun/agents/tools/codebase/file_read.py +11 -2
- shotgun/agents/tools/codebase/query_graph.py +6 -0
- shotgun/agents/tools/codebase/retrieve_code.py +6 -0
- shotgun/agents/tools/file_management.py +27 -7
- shotgun/agents/tools/registry.py +217 -0
- shotgun/agents/tools/web_search/__init__.py +8 -8
- shotgun/agents/tools/web_search/anthropic.py +8 -2
- shotgun/agents/tools/web_search/gemini.py +7 -1
- shotgun/agents/tools/web_search/openai.py +8 -2
- shotgun/agents/tools/web_search/utils.py +2 -2
- shotgun/agents/usage_manager.py +16 -11
- shotgun/api_endpoints.py +7 -3
- shotgun/build_constants.py +2 -2
- shotgun/cli/clear.py +53 -0
- shotgun/cli/compact.py +188 -0
- shotgun/cli/config.py +8 -5
- shotgun/cli/context.py +154 -0
- shotgun/cli/error_handler.py +24 -0
- shotgun/cli/export.py +34 -34
- shotgun/cli/feedback.py +4 -2
- shotgun/cli/models.py +1 -0
- shotgun/cli/plan.py +34 -34
- shotgun/cli/research.py +18 -10
- shotgun/cli/spec/__init__.py +5 -0
- shotgun/cli/spec/backup.py +81 -0
- shotgun/cli/spec/commands.py +132 -0
- shotgun/cli/spec/models.py +48 -0
- shotgun/cli/spec/pull_service.py +219 -0
- shotgun/cli/specify.py +20 -19
- shotgun/cli/tasks.py +34 -34
- shotgun/cli/update.py +16 -2
- shotgun/codebase/core/change_detector.py +5 -3
- shotgun/codebase/core/code_retrieval.py +4 -2
- shotgun/codebase/core/ingestor.py +163 -15
- shotgun/codebase/core/manager.py +13 -4
- shotgun/codebase/core/nl_query.py +1 -1
- shotgun/codebase/models.py +2 -0
- shotgun/exceptions.py +357 -0
- shotgun/llm_proxy/__init__.py +17 -0
- shotgun/llm_proxy/client.py +215 -0
- shotgun/llm_proxy/models.py +137 -0
- shotgun/logging_config.py +60 -27
- shotgun/main.py +77 -11
- shotgun/posthog_telemetry.py +38 -29
- shotgun/prompts/agents/partials/common_agent_system_prompt.j2 +28 -2
- shotgun/prompts/agents/partials/interactive_mode.j2 +3 -3
- shotgun/prompts/agents/plan.j2 +16 -0
- shotgun/prompts/agents/research.j2 +16 -3
- shotgun/prompts/agents/specify.j2 +54 -1
- shotgun/prompts/agents/state/system_state.j2 +0 -2
- shotgun/prompts/agents/tasks.j2 +16 -0
- shotgun/prompts/history/chunk_summarization.j2 +34 -0
- shotgun/prompts/history/combine_summaries.j2 +53 -0
- shotgun/sdk/codebase.py +14 -3
- shotgun/sentry_telemetry.py +163 -16
- shotgun/settings.py +243 -0
- shotgun/shotgun_web/__init__.py +67 -1
- shotgun/shotgun_web/client.py +42 -1
- shotgun/shotgun_web/constants.py +46 -0
- shotgun/shotgun_web/exceptions.py +29 -0
- shotgun/shotgun_web/models.py +390 -0
- shotgun/shotgun_web/shared_specs/__init__.py +32 -0
- shotgun/shotgun_web/shared_specs/file_scanner.py +175 -0
- shotgun/shotgun_web/shared_specs/hasher.py +83 -0
- shotgun/shotgun_web/shared_specs/models.py +71 -0
- shotgun/shotgun_web/shared_specs/upload_pipeline.py +329 -0
- shotgun/shotgun_web/shared_specs/utils.py +34 -0
- shotgun/shotgun_web/specs_client.py +703 -0
- shotgun/shotgun_web/supabase_client.py +31 -0
- shotgun/telemetry.py +10 -33
- shotgun/tui/app.py +310 -46
- shotgun/tui/commands/__init__.py +1 -1
- shotgun/tui/components/context_indicator.py +179 -0
- shotgun/tui/components/mode_indicator.py +70 -0
- shotgun/tui/components/status_bar.py +48 -0
- shotgun/tui/containers.py +91 -0
- shotgun/tui/dependencies.py +39 -0
- shotgun/tui/layout.py +5 -0
- shotgun/tui/protocols.py +45 -0
- shotgun/tui/screens/chat/__init__.py +5 -0
- shotgun/tui/screens/chat/chat.tcss +54 -0
- shotgun/tui/screens/chat/chat_screen.py +1531 -0
- shotgun/tui/screens/chat/codebase_index_prompt_screen.py +243 -0
- shotgun/tui/screens/chat/codebase_index_selection.py +12 -0
- shotgun/tui/screens/chat/help_text.py +40 -0
- shotgun/tui/screens/chat/prompt_history.py +48 -0
- shotgun/tui/screens/chat.tcss +11 -0
- shotgun/tui/screens/chat_screen/command_providers.py +91 -4
- shotgun/tui/screens/chat_screen/hint_message.py +76 -1
- shotgun/tui/screens/chat_screen/history/__init__.py +22 -0
- shotgun/tui/screens/chat_screen/history/agent_response.py +66 -0
- shotgun/tui/screens/chat_screen/history/chat_history.py +115 -0
- shotgun/tui/screens/chat_screen/history/formatters.py +115 -0
- shotgun/tui/screens/chat_screen/history/partial_response.py +43 -0
- shotgun/tui/screens/chat_screen/history/user_question.py +42 -0
- shotgun/tui/screens/confirmation_dialog.py +191 -0
- shotgun/tui/screens/directory_setup.py +45 -41
- shotgun/tui/screens/feedback.py +14 -7
- shotgun/tui/screens/github_issue.py +111 -0
- shotgun/tui/screens/model_picker.py +77 -32
- shotgun/tui/screens/onboarding.py +580 -0
- shotgun/tui/screens/pipx_migration.py +205 -0
- shotgun/tui/screens/provider_config.py +116 -35
- shotgun/tui/screens/shared_specs/__init__.py +21 -0
- shotgun/tui/screens/shared_specs/create_spec_dialog.py +273 -0
- shotgun/tui/screens/shared_specs/models.py +56 -0
- shotgun/tui/screens/shared_specs/share_specs_dialog.py +390 -0
- shotgun/tui/screens/shared_specs/upload_progress_screen.py +452 -0
- shotgun/tui/screens/shotgun_auth.py +112 -18
- shotgun/tui/screens/spec_pull.py +288 -0
- shotgun/tui/screens/welcome.py +137 -11
- shotgun/tui/services/__init__.py +5 -0
- shotgun/tui/services/conversation_service.py +187 -0
- shotgun/tui/state/__init__.py +7 -0
- shotgun/tui/state/processing_state.py +185 -0
- shotgun/tui/utils/mode_progress.py +14 -7
- shotgun/tui/widgets/__init__.py +5 -0
- shotgun/tui/widgets/widget_coordinator.py +263 -0
- shotgun/utils/file_system_utils.py +22 -2
- shotgun/utils/marketing.py +110 -0
- shotgun/utils/update_checker.py +69 -14
- shotgun_sh-0.3.3.dev1.dist-info/METADATA +472 -0
- shotgun_sh-0.3.3.dev1.dist-info/RECORD +229 -0
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/WHEEL +1 -1
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/entry_points.txt +1 -0
- {shotgun_sh-0.2.8.dev2.dist-info → shotgun_sh-0.3.3.dev1.dist-info}/licenses/LICENSE +1 -1
- shotgun/tui/screens/chat.py +0 -996
- shotgun/tui/screens/chat_screen/history.py +0 -335
- shotgun_sh-0.2.8.dev2.dist-info/METADATA +0 -126
- shotgun_sh-0.2.8.dev2.dist-info/RECORD +0 -155
- /shotgun/agents/{history → conversation/history}/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/context_extraction.py +0 -0
- /shotgun/agents/{history → conversation/history}/history_building.py +0 -0
- /shotgun/agents/{history → conversation/history}/message_utils.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_counting/__init__.py +0 -0
- /shotgun/agents/{history → conversation/history}/token_estimation.py +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Pydantic models for the shared specs upload pipeline."""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel
|
|
4
|
+
|
|
5
|
+
from shotgun.shotgun_web.models import FileMetadata
|
|
6
|
+
from shotgun.shotgun_web.shared_specs.utils import UploadPhase
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class UploadProgress(BaseModel):
|
|
10
|
+
"""Progress information for the upload pipeline.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
phase: Current phase of the pipeline
|
|
14
|
+
current: Current item number in the phase
|
|
15
|
+
total: Total items in the phase
|
|
16
|
+
current_file: Name of the file currently being processed
|
|
17
|
+
bytes_uploaded: Total bytes uploaded so far
|
|
18
|
+
total_bytes: Total bytes to upload
|
|
19
|
+
message: Human-readable status message
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
phase: UploadPhase
|
|
23
|
+
current: int = 0
|
|
24
|
+
total: int = 0
|
|
25
|
+
current_file: str | None = None
|
|
26
|
+
bytes_uploaded: int = 0
|
|
27
|
+
total_bytes: int = 0
|
|
28
|
+
message: str = ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class UploadResult(BaseModel):
|
|
32
|
+
"""Result of the upload pipeline.
|
|
33
|
+
|
|
34
|
+
Attributes:
|
|
35
|
+
success: Whether the upload completed successfully
|
|
36
|
+
web_url: URL to view the spec version (on success)
|
|
37
|
+
error: Error message (on failure)
|
|
38
|
+
files_uploaded: Number of files uploaded
|
|
39
|
+
total_bytes: Total bytes uploaded
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
success: bool
|
|
43
|
+
web_url: str | None = None
|
|
44
|
+
error: str | None = None
|
|
45
|
+
files_uploaded: int = 0
|
|
46
|
+
total_bytes: int = 0
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class FileWithHash(BaseModel):
|
|
50
|
+
"""File metadata with computed hash."""
|
|
51
|
+
|
|
52
|
+
metadata: FileMetadata
|
|
53
|
+
content_hash: str = ""
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class UploadState(BaseModel):
|
|
57
|
+
"""Internal state for upload progress tracking."""
|
|
58
|
+
|
|
59
|
+
files_uploaded: int = 0
|
|
60
|
+
bytes_uploaded: int = 0
|
|
61
|
+
total_bytes: int = 0
|
|
62
|
+
current_file: str | None = None
|
|
63
|
+
hashes_completed: int = 0
|
|
64
|
+
total_files: int = 0
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class ScanResult(BaseModel):
|
|
68
|
+
"""Result of scanning .shotgun/ directory."""
|
|
69
|
+
|
|
70
|
+
files: list[FileMetadata]
|
|
71
|
+
total_files_before_filter: int
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""Upload pipeline for .shotgun/ directory to Specs API."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
import time
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from shotgun.logging_config import get_logger
|
|
9
|
+
from shotgun.posthog_telemetry import track_event
|
|
10
|
+
from shotgun.shotgun_web.models import FileMetadata
|
|
11
|
+
from shotgun.shotgun_web.shared_specs.file_scanner import (
|
|
12
|
+
scan_shotgun_directory_with_counts,
|
|
13
|
+
)
|
|
14
|
+
from shotgun.shotgun_web.shared_specs.hasher import calculate_sha256
|
|
15
|
+
from shotgun.shotgun_web.shared_specs.models import (
|
|
16
|
+
FileWithHash,
|
|
17
|
+
UploadProgress,
|
|
18
|
+
UploadResult,
|
|
19
|
+
UploadState,
|
|
20
|
+
)
|
|
21
|
+
from shotgun.shotgun_web.shared_specs.utils import UploadPhase, format_bytes
|
|
22
|
+
from shotgun.shotgun_web.specs_client import SpecsClient
|
|
23
|
+
|
|
24
|
+
logger = get_logger(__name__)
|
|
25
|
+
|
|
26
|
+
# Maximum concurrent hash calculations
|
|
27
|
+
MAX_CONCURRENT_HASHES = 10
|
|
28
|
+
|
|
29
|
+
# Maximum concurrent file uploads
|
|
30
|
+
MAX_CONCURRENT_UPLOADS = 3
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
async def run_upload_pipeline(
|
|
34
|
+
workspace_id: str,
|
|
35
|
+
spec_id: str,
|
|
36
|
+
version_id: str,
|
|
37
|
+
project_root: Path | None = None,
|
|
38
|
+
on_progress: Callable[[UploadProgress], None] | None = None,
|
|
39
|
+
) -> UploadResult:
|
|
40
|
+
"""Run the complete upload pipeline for a spec version.
|
|
41
|
+
|
|
42
|
+
Scans the .shotgun/ directory, calculates hashes for all files,
|
|
43
|
+
uploads them to the API, and closes the version.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
workspace_id: Workspace UUID
|
|
47
|
+
spec_id: Spec UUID
|
|
48
|
+
version_id: Version UUID
|
|
49
|
+
project_root: Project root containing .shotgun/ directory (defaults to cwd)
|
|
50
|
+
on_progress: Optional callback for progress updates
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
UploadResult with success status and web URL or error message
|
|
54
|
+
"""
|
|
55
|
+
if project_root is None:
|
|
56
|
+
project_root = Path.cwd()
|
|
57
|
+
|
|
58
|
+
state = UploadState()
|
|
59
|
+
start_time = time.time()
|
|
60
|
+
current_phase: UploadPhase = UploadPhase.CREATING
|
|
61
|
+
track_event("spec_upload_started")
|
|
62
|
+
|
|
63
|
+
def report_progress(progress: UploadProgress) -> None:
|
|
64
|
+
"""Report progress to callback if provided."""
|
|
65
|
+
if on_progress:
|
|
66
|
+
on_progress(progress)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
# Phase 1: Scan files
|
|
70
|
+
current_phase = UploadPhase.SCANNING
|
|
71
|
+
report_progress(
|
|
72
|
+
UploadProgress(
|
|
73
|
+
phase=UploadPhase.SCANNING,
|
|
74
|
+
message="Scanning .shotgun/ directory...",
|
|
75
|
+
)
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
scan_result = await scan_shotgun_directory_with_counts(project_root)
|
|
79
|
+
files = scan_result.files
|
|
80
|
+
state.total_files = len(files)
|
|
81
|
+
|
|
82
|
+
if not files:
|
|
83
|
+
# Distinguish between empty directory and all files filtered
|
|
84
|
+
if scan_result.total_files_before_filter > 0:
|
|
85
|
+
error_message = (
|
|
86
|
+
"No shareable files found. All files matched ignore patterns."
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
error_message = (
|
|
90
|
+
"No files to share. Add specifications to .shotgun/ first."
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
track_event(
|
|
94
|
+
"spec_upload_failed",
|
|
95
|
+
{
|
|
96
|
+
"error_type": "EmptyDirectory",
|
|
97
|
+
"phase": current_phase.value,
|
|
98
|
+
"files_uploaded": 0,
|
|
99
|
+
"bytes_uploaded": 0,
|
|
100
|
+
},
|
|
101
|
+
)
|
|
102
|
+
report_progress(
|
|
103
|
+
UploadProgress(
|
|
104
|
+
phase=UploadPhase.ERROR,
|
|
105
|
+
message=error_message,
|
|
106
|
+
)
|
|
107
|
+
)
|
|
108
|
+
return UploadResult(
|
|
109
|
+
success=False,
|
|
110
|
+
files_uploaded=0,
|
|
111
|
+
total_bytes=0,
|
|
112
|
+
error=error_message,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Calculate total size
|
|
116
|
+
state.total_bytes = sum(f.size_bytes for f in files)
|
|
117
|
+
|
|
118
|
+
report_progress(
|
|
119
|
+
UploadProgress(
|
|
120
|
+
phase=UploadPhase.SCANNING,
|
|
121
|
+
total=state.total_files,
|
|
122
|
+
total_bytes=state.total_bytes,
|
|
123
|
+
message=f"Found {state.total_files} files ({format_bytes(state.total_bytes)})",
|
|
124
|
+
)
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Phase 2: Calculate hashes
|
|
128
|
+
current_phase = UploadPhase.HASHING
|
|
129
|
+
report_progress(
|
|
130
|
+
UploadProgress(
|
|
131
|
+
phase=UploadPhase.HASHING,
|
|
132
|
+
current=0,
|
|
133
|
+
total=state.total_files,
|
|
134
|
+
message="Calculating file hashes...",
|
|
135
|
+
)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
files_with_hashes = await _calculate_hashes(files, state, report_progress)
|
|
139
|
+
|
|
140
|
+
# Phase 3: Upload files
|
|
141
|
+
current_phase = UploadPhase.UPLOADING
|
|
142
|
+
report_progress(
|
|
143
|
+
UploadProgress(
|
|
144
|
+
phase=UploadPhase.UPLOADING,
|
|
145
|
+
current=0,
|
|
146
|
+
total=state.total_files,
|
|
147
|
+
total_bytes=state.total_bytes,
|
|
148
|
+
message="Uploading files...",
|
|
149
|
+
)
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
client = SpecsClient()
|
|
153
|
+
await _upload_files(
|
|
154
|
+
client,
|
|
155
|
+
workspace_id,
|
|
156
|
+
spec_id,
|
|
157
|
+
version_id,
|
|
158
|
+
files_with_hashes,
|
|
159
|
+
state,
|
|
160
|
+
report_progress,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Phase 4: Close version
|
|
164
|
+
current_phase = UploadPhase.CLOSING
|
|
165
|
+
report_progress(
|
|
166
|
+
UploadProgress(
|
|
167
|
+
phase=UploadPhase.CLOSING,
|
|
168
|
+
current=state.files_uploaded,
|
|
169
|
+
total=state.total_files,
|
|
170
|
+
bytes_uploaded=state.bytes_uploaded,
|
|
171
|
+
total_bytes=state.total_bytes,
|
|
172
|
+
message="Finalizing version...",
|
|
173
|
+
)
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
close_response = await client.close_version(workspace_id, spec_id, version_id)
|
|
177
|
+
|
|
178
|
+
# Complete
|
|
179
|
+
report_progress(
|
|
180
|
+
UploadProgress(
|
|
181
|
+
phase=UploadPhase.COMPLETE,
|
|
182
|
+
current=state.files_uploaded,
|
|
183
|
+
total=state.total_files,
|
|
184
|
+
bytes_uploaded=state.bytes_uploaded,
|
|
185
|
+
total_bytes=state.total_bytes,
|
|
186
|
+
message="Upload complete!",
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Track successful completion
|
|
191
|
+
duration = time.time() - start_time
|
|
192
|
+
track_event(
|
|
193
|
+
"spec_upload_completed",
|
|
194
|
+
{
|
|
195
|
+
"file_count": state.files_uploaded,
|
|
196
|
+
"total_bytes": state.bytes_uploaded,
|
|
197
|
+
"duration_seconds": round(duration, 2),
|
|
198
|
+
},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
return UploadResult(
|
|
202
|
+
success=True,
|
|
203
|
+
web_url=close_response.web_url,
|
|
204
|
+
files_uploaded=state.files_uploaded,
|
|
205
|
+
total_bytes=state.bytes_uploaded,
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
except Exception as e:
|
|
209
|
+
logger.error(f"Upload pipeline failed: {e}", exc_info=True)
|
|
210
|
+
track_event(
|
|
211
|
+
"spec_upload_failed",
|
|
212
|
+
{
|
|
213
|
+
"error_type": type(e).__name__,
|
|
214
|
+
"phase": current_phase.value,
|
|
215
|
+
"files_uploaded": state.files_uploaded,
|
|
216
|
+
"bytes_uploaded": state.bytes_uploaded,
|
|
217
|
+
},
|
|
218
|
+
)
|
|
219
|
+
report_progress(
|
|
220
|
+
UploadProgress(
|
|
221
|
+
phase=UploadPhase.ERROR,
|
|
222
|
+
current=state.files_uploaded,
|
|
223
|
+
total=state.total_files,
|
|
224
|
+
bytes_uploaded=state.bytes_uploaded,
|
|
225
|
+
total_bytes=state.total_bytes,
|
|
226
|
+
message=f"Upload failed: {e}",
|
|
227
|
+
)
|
|
228
|
+
)
|
|
229
|
+
return UploadResult(
|
|
230
|
+
success=False,
|
|
231
|
+
error=str(e),
|
|
232
|
+
files_uploaded=state.files_uploaded,
|
|
233
|
+
total_bytes=state.bytes_uploaded,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
async def _calculate_hashes(
|
|
238
|
+
files: list[FileMetadata],
|
|
239
|
+
state: UploadState,
|
|
240
|
+
report_progress: Callable[[UploadProgress], None],
|
|
241
|
+
) -> list[FileWithHash]:
|
|
242
|
+
"""Calculate hashes for all files with progress reporting.
|
|
243
|
+
|
|
244
|
+
Uses semaphore to limit concurrent hash operations.
|
|
245
|
+
"""
|
|
246
|
+
semaphore = asyncio.Semaphore(MAX_CONCURRENT_HASHES)
|
|
247
|
+
files_with_hashes: list[FileWithHash] = []
|
|
248
|
+
lock = asyncio.Lock()
|
|
249
|
+
|
|
250
|
+
async def hash_file(file_meta: FileMetadata) -> FileWithHash:
|
|
251
|
+
async with semaphore:
|
|
252
|
+
content_hash = await calculate_sha256(file_meta.absolute_path)
|
|
253
|
+
|
|
254
|
+
# Update progress
|
|
255
|
+
async with lock:
|
|
256
|
+
state.hashes_completed += 1
|
|
257
|
+
report_progress(
|
|
258
|
+
UploadProgress(
|
|
259
|
+
phase=UploadPhase.HASHING,
|
|
260
|
+
current=state.hashes_completed,
|
|
261
|
+
total=state.total_files,
|
|
262
|
+
current_file=file_meta.relative_path,
|
|
263
|
+
message=f"Hashing {file_meta.relative_path}",
|
|
264
|
+
)
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
return FileWithHash(metadata=file_meta, content_hash=content_hash)
|
|
268
|
+
|
|
269
|
+
# Run hash calculations concurrently
|
|
270
|
+
results = await asyncio.gather(*[hash_file(f) for f in files])
|
|
271
|
+
files_with_hashes = list(results)
|
|
272
|
+
|
|
273
|
+
return files_with_hashes
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
async def _upload_files(
|
|
277
|
+
client: SpecsClient,
|
|
278
|
+
workspace_id: str,
|
|
279
|
+
spec_id: str,
|
|
280
|
+
version_id: str,
|
|
281
|
+
files: list[FileWithHash],
|
|
282
|
+
state: UploadState,
|
|
283
|
+
report_progress: Callable[[UploadProgress], None],
|
|
284
|
+
) -> None:
|
|
285
|
+
"""Upload all files with progress reporting.
|
|
286
|
+
|
|
287
|
+
Uses semaphore to limit concurrent uploads.
|
|
288
|
+
"""
|
|
289
|
+
semaphore = asyncio.Semaphore(MAX_CONCURRENT_UPLOADS)
|
|
290
|
+
lock = asyncio.Lock()
|
|
291
|
+
|
|
292
|
+
async def upload_file(file: FileWithHash) -> None:
|
|
293
|
+
async with semaphore:
|
|
294
|
+
# Initiate upload to get presigned URL
|
|
295
|
+
response = await client.initiate_file_upload(
|
|
296
|
+
workspace_id,
|
|
297
|
+
spec_id,
|
|
298
|
+
version_id,
|
|
299
|
+
file.metadata.relative_path,
|
|
300
|
+
file.metadata.size_bytes,
|
|
301
|
+
file.content_hash,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
# Upload to presigned URL
|
|
305
|
+
await client.upload_file_to_presigned_url(
|
|
306
|
+
response.upload_url,
|
|
307
|
+
file.metadata.absolute_path,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Update progress
|
|
311
|
+
async with lock:
|
|
312
|
+
state.files_uploaded += 1
|
|
313
|
+
state.bytes_uploaded += file.metadata.size_bytes
|
|
314
|
+
state.current_file = file.metadata.relative_path
|
|
315
|
+
|
|
316
|
+
report_progress(
|
|
317
|
+
UploadProgress(
|
|
318
|
+
phase=UploadPhase.UPLOADING,
|
|
319
|
+
current=state.files_uploaded,
|
|
320
|
+
total=state.total_files,
|
|
321
|
+
current_file=file.metadata.relative_path,
|
|
322
|
+
bytes_uploaded=state.bytes_uploaded,
|
|
323
|
+
total_bytes=state.total_bytes,
|
|
324
|
+
message=f"Uploaded {file.metadata.relative_path}",
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Run uploads concurrently
|
|
329
|
+
await asyncio.gather(*[upload_file(f) for f in files])
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Utility functions for shared specs module."""
|
|
2
|
+
|
|
3
|
+
from enum import StrEnum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class UploadPhase(StrEnum):
|
|
7
|
+
"""Upload pipeline phases."""
|
|
8
|
+
|
|
9
|
+
CREATING = "creating" # Creating spec/version via API
|
|
10
|
+
SCANNING = "scanning"
|
|
11
|
+
HASHING = "hashing"
|
|
12
|
+
UPLOADING = "uploading"
|
|
13
|
+
CLOSING = "closing"
|
|
14
|
+
COMPLETE = "complete"
|
|
15
|
+
ERROR = "error"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def format_bytes(size: int) -> str:
|
|
19
|
+
"""Format bytes as human-readable string.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
size: Size in bytes
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Human-readable string like "1.5 KB" or "2.3 MB"
|
|
26
|
+
"""
|
|
27
|
+
if size < 1024:
|
|
28
|
+
return f"{size} B"
|
|
29
|
+
elif size < 1024 * 1024:
|
|
30
|
+
return f"{size / 1024:.1f} KB"
|
|
31
|
+
elif size < 1024 * 1024 * 1024:
|
|
32
|
+
return f"{size / (1024 * 1024):.1f} MB"
|
|
33
|
+
else:
|
|
34
|
+
return f"{size / (1024 * 1024 * 1024):.1f} GB"
|