code-mower 0.5.0b5__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.
- code_mower/__init__.py +3 -0
- code_mower/adapters/__init__.py +39 -0
- code_mower/adapters/_base.py +148 -0
- code_mower/adapters/cursor_bugbot.py +97 -0
- code_mower/adapters/gitar.py +111 -0
- code_mower/adapters/greptile.py +191 -0
- code_mower/adapters/qodo.py +140 -0
- code_mower/antigravity_cli_audit_pr.py +241 -0
- code_mower/audit_handoff_log.py +345 -0
- code_mower/audit_labeler_lib.py +311 -0
- code_mower/audit_progress.py +203 -0
- code_mower/blind_review_artifacts.py +562 -0
- code_mower/blind_review_coordinator.py +276 -0
- code_mower/bootstrap.py +524 -0
- code_mower/builder_experiment.py +539 -0
- code_mower/calibration/__init__.py +181 -0
- code_mower/calibration/arms.py +267 -0
- code_mower/calibration/auto_discovery.py +348 -0
- code_mower/calibration/commands.py +192 -0
- code_mower/calibration/context_inputs.py +199 -0
- code_mower/calibration/corpus.py +91 -0
- code_mower/calibration/evidence.py +20 -0
- code_mower/calibration/evidence_report.py +360 -0
- code_mower/calibration/identity.py +27 -0
- code_mower/calibration/metrics.py +18 -0
- code_mower/calibration/overlap.py +103 -0
- code_mower/calibration/planning.py +309 -0
- code_mower/calibration/policy.py +207 -0
- code_mower/calibration/results.py +315 -0
- code_mower/calibration/run_results.py +147 -0
- code_mower/calibration/run_status.py +64 -0
- code_mower/calibration/runner.py +360 -0
- code_mower/calibration/truth.py +188 -0
- code_mower/calibration/value_report.py +142 -0
- code_mower/checks.py +402 -0
- code_mower/claude_audit_pr.py +1126 -0
- code_mower/claude_cli_bounce.py +303 -0
- code_mower/claude_cli_environment.py +73 -0
- code_mower/clear_stale.py +374 -0
- code_mower/cli.py +537 -0
- code_mower/cloud.py +674 -0
- code_mower/cloud_client/__init__.py +173 -0
- code_mower/cloud_client/bundle.py +155 -0
- code_mower/cloud_client/doctor.py +206 -0
- code_mower/cloud_client/dogfood.py +78 -0
- code_mower/cloud_client/endpoints.py +114 -0
- code_mower/cloud_client/errors.py +7 -0
- code_mower/cloud_client/events.py +278 -0
- code_mower/cloud_client/export.py +272 -0
- code_mower/cloud_client/git_metadata.py +46 -0
- code_mower/cloud_client/manifest.py +39 -0
- code_mower/cloud_client/operations.py +448 -0
- code_mower/cloud_client/reports.py +45 -0
- code_mower/cloud_client/setup.py +205 -0
- code_mower/cloud_client/upload.py +97 -0
- code_mower/code_mower_calibration.py +598 -0
- code_mower/code_mower_context_packs.py +591 -0
- code_mower/code_mower_merge.py +227 -0
- code_mower/code_mower_telemetry.py +561 -0
- code_mower/coderabbit_cli_audit_pr.py +526 -0
- code_mower/codex_audit_env_preflight.py +220 -0
- code_mower/codex_audit_pr.py +1738 -0
- code_mower/codex_audit_schema_smoke.py +160 -0
- code_mower/codex_audit_verdict.schema.json +44 -0
- code_mower/config.py +655 -0
- code_mower/doctor.py +161 -0
- code_mower/doctor_checks/__init__.py +104 -0
- code_mower/doctor_checks/cloud.py +129 -0
- code_mower/doctor_checks/common.py +215 -0
- code_mower/doctor_checks/github.py +128 -0
- code_mower/doctor_checks/github_actions.py +11 -0
- code_mower/doctor_checks/github_actions_cost.py +99 -0
- code_mower/doctor_checks/github_actions_cost_summary.py +111 -0
- code_mower/doctor_checks/github_actions_failure_annotations.py +27 -0
- code_mower/doctor_checks/github_actions_failure_models.py +47 -0
- code_mower/doctor_checks/github_actions_failure_scan.py +200 -0
- code_mower/doctor_checks/github_actions_failure_selection.py +63 -0
- code_mower/doctor_checks/github_actions_failures.py +103 -0
- code_mower/doctor_checks/github_actions_permissions.py +55 -0
- code_mower/doctor_checks/github_api.py +79 -0
- code_mower/doctor_checks/github_branch.py +56 -0
- code_mower/doctor_checks/github_config.py +25 -0
- code_mower/doctor_checks/github_provider.py +61 -0
- code_mower/doctor_checks/github_repo.py +120 -0
- code_mower/doctor_checks/groups.py +36 -0
- code_mower/doctor_checks/models.py +96 -0
- code_mower/doctor_checks/output.py +86 -0
- code_mower/doctor_checks/presets.py +64 -0
- code_mower/doctor_checks/privacy.py +20 -0
- code_mower/doctor_checks/provider_api_model.py +138 -0
- code_mower/doctor_checks/provider_api_model_openai.py +29 -0
- code_mower/doctor_checks/provider_api_model_profiles.py +137 -0
- code_mower/doctor_checks/provider_env.py +113 -0
- code_mower/doctor_checks/provider_env_required.py +56 -0
- code_mower/doctor_checks/provider_env_tokens.py +100 -0
- code_mower/doctor_checks/provider_local_cli.py +162 -0
- code_mower/doctor_checks/provider_local_cli_commands.py +47 -0
- code_mower/doctor_checks/provider_local_cli_probe_config.py +70 -0
- code_mower/doctor_checks/provider_probe.py +20 -0
- code_mower/doctor_checks/provider_probe_auth.py +52 -0
- code_mower/doctor_checks/provider_probe_evaluation.py +109 -0
- code_mower/doctor_checks/provider_probe_json.py +45 -0
- code_mower/doctor_checks/provider_probe_remediation.py +39 -0
- code_mower/doctor_checks/providers.py +159 -0
- code_mower/doctor_checks/registry.py +69 -0
- code_mower/doctor_checks/runner.py +188 -0
- code_mower/doctor_checks/runtime.py +89 -0
- code_mower/doctor_checks/runtime_github_auth.py +148 -0
- code_mower/gemini_cli_audit_pr.py +897 -0
- code_mower/hermes_cli_audit_pr.py +436 -0
- code_mower/init.py +888 -0
- code_mower/lane_configs/__init__.py +37 -0
- code_mower/lane_configs/aider.py +32 -0
- code_mower/lane_configs/antigravity_cli.py +35 -0
- code_mower/lane_configs/claude.py +35 -0
- code_mower/lane_configs/codex.py +32 -0
- code_mower/lane_configs/devin.py +33 -0
- code_mower/lane_configs/gemini_cli.py +35 -0
- code_mower/lane_configs/hermes_cli.py +35 -0
- code_mower/lane_configs/local_llm.py +31 -0
- code_mower/local_llm_audit_pr.py +1364 -0
- code_mower/local_llm_bakeoff.py +458 -0
- code_mower/local_llm_calibration.py +441 -0
- code_mower/local_llm_profiles.py +66 -0
- code_mower/migration.py +508 -0
- code_mower/migration_install.py +292 -0
- code_mower/migration_mirror.py +392 -0
- code_mower/migration_readiness.py +237 -0
- code_mower/migration_rehearsal.py +718 -0
- code_mower/next_steps.py +441 -0
- code_mower/package.py +673 -0
- code_mower/package_content.py +444 -0
- code_mower/package_manifest.py +452 -0
- code_mower/package_paths.py +53 -0
- code_mower/package_rendering.py +90 -0
- code_mower/package_static.py +585 -0
- code_mower/prompts.py +267 -0
- code_mower/provider_registry.py +469 -0
- code_mower/provider_runners/__init__.py +60 -0
- code_mower/provider_runners/comments.py +31 -0
- code_mower/provider_runners/git.py +46 -0
- code_mower/provider_runners/github_auth.py +61 -0
- code_mower/provider_runners/github_pr.py +120 -0
- code_mower/provider_runners/process.py +58 -0
- code_mower/provider_runners/repo_paths.py +23 -0
- code_mower/provider_runners/text_schema.py +41 -0
- code_mower/provider_runners/verdict_artifacts.py +103 -0
- code_mower/provider_runners/workspace.py +57 -0
- code_mower/release_readiness.py +549 -0
- code_mower/reviewer_metrics.py +389 -0
- code_mower/saas_reviewer_labeler.py +809 -0
- code_mower/secrets.py +89 -0
- code_mower/templates/builder-experiment.example.json +55 -0
- code_mower/templates/calibration-corpus.example.json +129 -0
- code_mower/templates/calibration-corpus.json +129 -0
- code_mower/templates/code-mower.example.yml +423 -0
- code_mower/templates/context-packs.example.json +150 -0
- code_mower/templates/lane_prompts/base-audit.md +22 -0
- code_mower/templates/lane_prompts/calibration-policy.md +21 -0
- code_mower/templates/lane_prompts/context-driven-quality.md +21 -0
- code_mower/templates/lane_prompts/docs-design.md +12 -0
- code_mower/templates/lane_prompts/generic-programming.md +21 -0
- code_mower/templates/lane_prompts/operability.md +22 -0
- code_mower/templates/lane_prompts/package-runtime.md +12 -0
- code_mower/templates/lane_prompts/security-threat-model.md +22 -0
- code_mower/templates/product-support/code_mower +216 -0
- code_mower/templates/product-support/code_mower_standalone_pin.env +7 -0
- code_mower/templates/product-support/code_mower_standalone_shadow.sh +151 -0
- code_mower/templates/product-support/run_claude_audit_pr.sh +32 -0
- code_mower/templates/product-support/run_codex_audit_pr.sh +32 -0
- code_mower/templates/product-support/safe_gh_comment.py +96 -0
- code_mower/templates/providers.yml +454 -0
- code_mower/templates/reviewer-spend.example.json +28 -0
- code_mower/templates/reviewer-value-report.example.md +20 -0
- code_mower/templates/workflows/private-standalone-shadow.yml.j2 +106 -0
- code_mower/templates/workflows/review-clear-stale.yml.j2 +83 -0
- code_mower/trailer_comment_labeler.py +207 -0
- code_mower/versioning.py +32 -0
- code_mower-0.5.0b5.dist-info/METADATA +302 -0
- code_mower-0.5.0b5.dist-info/RECORD +185 -0
- code_mower-0.5.0b5.dist-info/WHEEL +5 -0
- code_mower-0.5.0b5.dist-info/entry_points.txt +2 -0
- code_mower-0.5.0b5.dist-info/licenses/LICENSE +202 -0
- code_mower-0.5.0b5.dist-info/licenses/NOTICE +10 -0
- code_mower-0.5.0b5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""Local token/config handling for CodeMower.com setup flows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shlex
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from .endpoints import validate_upload_endpoint
|
|
11
|
+
from .errors import CloudBundleError
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DEFAULT_UPLOAD_ENDPOINT = "https://codemower.com/api/ingest"
|
|
15
|
+
DEFAULT_TOKEN_ENV = "CODE_MOWER_CLOUD_TOKEN"
|
|
16
|
+
DEFAULT_TEAM_ID_ENV = "CODE_MOWER_CLOUD_TEAM_ID"
|
|
17
|
+
DEFAULT_INSTALL_ID_ENV = "CODE_MOWER_INSTALL_ID"
|
|
18
|
+
DEFAULT_SETUP_INSTALL_ID = "code-mower-local"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def token_prefix(token: str) -> str:
|
|
22
|
+
token = token.strip()
|
|
23
|
+
if not token:
|
|
24
|
+
return ""
|
|
25
|
+
visible = min(12, max(4, len(token) // 2), max(0, len(token) - 4))
|
|
26
|
+
if visible <= 0:
|
|
27
|
+
return "<redacted>"
|
|
28
|
+
return token[:visible] + "..."
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def safe_config_stem(value: str) -> str:
|
|
32
|
+
stem = "".join(
|
|
33
|
+
char if char.isalnum() or char in {"-", "_", "."} else "-"
|
|
34
|
+
for char in value.strip()
|
|
35
|
+
)
|
|
36
|
+
return stem.strip("-_.") or DEFAULT_SETUP_INSTALL_ID
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def default_setup_path(install_id: str) -> Path:
|
|
40
|
+
return (
|
|
41
|
+
Path.home()
|
|
42
|
+
/ ".config"
|
|
43
|
+
/ "code-mower"
|
|
44
|
+
/ "tokens"
|
|
45
|
+
/ f"{safe_config_stem(install_id)}.env"
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def read_token_file(path: Path) -> str:
|
|
50
|
+
source = path.expanduser()
|
|
51
|
+
if not source.is_file():
|
|
52
|
+
raise CloudBundleError(f"token file does not exist or is not a file: {source}")
|
|
53
|
+
try:
|
|
54
|
+
text = source.read_text(encoding="utf-8")
|
|
55
|
+
except UnicodeDecodeError as exc:
|
|
56
|
+
raise CloudBundleError(f"token file is not UTF-8 text: {source}") from exc
|
|
57
|
+
except OSError as exc:
|
|
58
|
+
raise CloudBundleError(f"unable to read token file {source}: {exc}") from exc
|
|
59
|
+
for line in text.splitlines():
|
|
60
|
+
stripped = line.strip()
|
|
61
|
+
if not stripped or stripped.startswith("#"):
|
|
62
|
+
continue
|
|
63
|
+
if stripped.startswith("export "):
|
|
64
|
+
stripped = stripped.removeprefix("export ").strip()
|
|
65
|
+
if stripped.startswith(f"{DEFAULT_TOKEN_ENV}="):
|
|
66
|
+
return stripped.split("=", 1)[1].strip().strip("'\"")
|
|
67
|
+
return text.strip()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def resolve_setup_token(
|
|
71
|
+
*,
|
|
72
|
+
token: str,
|
|
73
|
+
token_file: Path | None,
|
|
74
|
+
token_stdin: bool,
|
|
75
|
+
token_env: str,
|
|
76
|
+
) -> str:
|
|
77
|
+
explicit_sources = sum(
|
|
78
|
+
1 for value in (bool(token), token_file is not None, token_stdin) if value
|
|
79
|
+
)
|
|
80
|
+
if explicit_sources > 1:
|
|
81
|
+
raise CloudBundleError(
|
|
82
|
+
"choose only one token source: --token, --token-file, or --token-stdin"
|
|
83
|
+
)
|
|
84
|
+
if token:
|
|
85
|
+
resolved = token.strip()
|
|
86
|
+
elif token_file is not None:
|
|
87
|
+
resolved = read_token_file(token_file)
|
|
88
|
+
elif token_stdin:
|
|
89
|
+
resolved = sys.stdin.read().strip()
|
|
90
|
+
else:
|
|
91
|
+
resolved = os.environ.get(token_env, "").strip()
|
|
92
|
+
if not resolved:
|
|
93
|
+
raise CloudBundleError(
|
|
94
|
+
"cloud setup needs a token; pass --token-stdin, --token-file, "
|
|
95
|
+
f"or set {token_env}"
|
|
96
|
+
)
|
|
97
|
+
return resolved
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def render_setup_env(
|
|
101
|
+
*,
|
|
102
|
+
token: str,
|
|
103
|
+
endpoint: str,
|
|
104
|
+
team_id: str,
|
|
105
|
+
install_id: str,
|
|
106
|
+
) -> str:
|
|
107
|
+
try:
|
|
108
|
+
validate_upload_endpoint(endpoint)
|
|
109
|
+
except ValueError as exc:
|
|
110
|
+
raise CloudBundleError(str(exc)) from exc
|
|
111
|
+
assignments = {
|
|
112
|
+
DEFAULT_TOKEN_ENV: token.strip(),
|
|
113
|
+
"CODE_MOWER_CLOUD_ENDPOINT": endpoint.strip(),
|
|
114
|
+
DEFAULT_TEAM_ID_ENV: team_id.strip(),
|
|
115
|
+
DEFAULT_INSTALL_ID_ENV: install_id.strip(),
|
|
116
|
+
}
|
|
117
|
+
lines = [
|
|
118
|
+
"# Code Mower Cloud local token file",
|
|
119
|
+
"# Keep this file private. It contains a bearer token.",
|
|
120
|
+
]
|
|
121
|
+
lines.extend(
|
|
122
|
+
f"export {name}={shlex.quote(value)}"
|
|
123
|
+
for name, value in assignments.items()
|
|
124
|
+
if value
|
|
125
|
+
)
|
|
126
|
+
return "\n".join(lines) + "\n"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def write_setup_env_file(
|
|
130
|
+
*,
|
|
131
|
+
path: Path,
|
|
132
|
+
text: str,
|
|
133
|
+
force: bool = False,
|
|
134
|
+
) -> None:
|
|
135
|
+
target = path.expanduser()
|
|
136
|
+
if target.exists() and not force:
|
|
137
|
+
raise CloudBundleError(
|
|
138
|
+
f"setup file already exists; pass --force to overwrite: {target}"
|
|
139
|
+
)
|
|
140
|
+
parent_existed = target.parent.exists()
|
|
141
|
+
try:
|
|
142
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
143
|
+
if not parent_existed:
|
|
144
|
+
target.parent.chmod(0o700)
|
|
145
|
+
except OSError as exc:
|
|
146
|
+
raise CloudBundleError(
|
|
147
|
+
f"unable to prepare setup directory {target.parent}: {exc}"
|
|
148
|
+
) from exc
|
|
149
|
+
flags = os.O_WRONLY | os.O_CREAT
|
|
150
|
+
if not force:
|
|
151
|
+
flags |= os.O_EXCL
|
|
152
|
+
try:
|
|
153
|
+
fd = os.open(target, flags, 0o600)
|
|
154
|
+
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
|
155
|
+
os.fchmod(handle.fileno(), 0o600)
|
|
156
|
+
handle.truncate(0)
|
|
157
|
+
handle.write(text)
|
|
158
|
+
target.chmod(0o600)
|
|
159
|
+
except FileExistsError as exc:
|
|
160
|
+
raise CloudBundleError(
|
|
161
|
+
f"setup file already exists; pass --force to overwrite: {target}"
|
|
162
|
+
) from exc
|
|
163
|
+
except OSError as exc:
|
|
164
|
+
raise CloudBundleError(f"unable to write setup file {target}: {exc}") from exc
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def run_cloud_setup(
|
|
168
|
+
*,
|
|
169
|
+
token: str,
|
|
170
|
+
token_file: Path | None,
|
|
171
|
+
token_stdin: bool,
|
|
172
|
+
token_env: str,
|
|
173
|
+
endpoint: str,
|
|
174
|
+
team_id: str,
|
|
175
|
+
install_id: str,
|
|
176
|
+
out: Path | None,
|
|
177
|
+
force: bool = False,
|
|
178
|
+
dry_run: bool = False,
|
|
179
|
+
) -> dict[str, str]:
|
|
180
|
+
resolved_install_id = install_id.strip() or DEFAULT_SETUP_INSTALL_ID
|
|
181
|
+
target = out.expanduser() if out else default_setup_path(resolved_install_id)
|
|
182
|
+
resolved_token = resolve_setup_token(
|
|
183
|
+
token=token,
|
|
184
|
+
token_file=token_file,
|
|
185
|
+
token_stdin=token_stdin,
|
|
186
|
+
token_env=token_env,
|
|
187
|
+
)
|
|
188
|
+
env_text = render_setup_env(
|
|
189
|
+
token=resolved_token,
|
|
190
|
+
endpoint=endpoint,
|
|
191
|
+
team_id=team_id,
|
|
192
|
+
install_id=resolved_install_id,
|
|
193
|
+
)
|
|
194
|
+
if not dry_run:
|
|
195
|
+
write_setup_env_file(path=target, text=env_text, force=force)
|
|
196
|
+
return {
|
|
197
|
+
"mode": "cloud-setup",
|
|
198
|
+
"status": "dry_run" if dry_run else "written",
|
|
199
|
+
"path": str(target),
|
|
200
|
+
"endpoint": endpoint,
|
|
201
|
+
"team_id": team_id,
|
|
202
|
+
"install_id": resolved_install_id,
|
|
203
|
+
"token_prefix": token_prefix(resolved_token),
|
|
204
|
+
"shell": f"source {shlex.quote(str(target))}",
|
|
205
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Upload payload construction and network posting for CodeMower.com."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import urllib.error
|
|
7
|
+
import urllib.request
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .bundle import validate_metadata_payload
|
|
12
|
+
from .endpoints import validate_upload_endpoint
|
|
13
|
+
from .errors import CloudBundleError
|
|
14
|
+
from .manifest import load_bundle_manifest
|
|
15
|
+
from .reports import included_report_payloads
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
UPLOAD_SCHEMA = "code_mower.cloudUpload.v1"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _validate_upload_endpoint(endpoint: str) -> None:
|
|
22
|
+
try:
|
|
23
|
+
validate_upload_endpoint(endpoint)
|
|
24
|
+
except ValueError as exc:
|
|
25
|
+
raise CloudBundleError(str(exc)) from exc
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_upload_payload(
|
|
29
|
+
*,
|
|
30
|
+
bundle_dir: Path,
|
|
31
|
+
include_reports: bool = False,
|
|
32
|
+
) -> dict[str, Any]:
|
|
33
|
+
bundle_dir = bundle_dir.expanduser()
|
|
34
|
+
if not bundle_dir.is_dir():
|
|
35
|
+
raise CloudBundleError(f"bundle directory does not exist: {bundle_dir}")
|
|
36
|
+
manifest = load_bundle_manifest(bundle_dir)
|
|
37
|
+
validate_metadata_payload(manifest)
|
|
38
|
+
events = manifest.get("events", [])
|
|
39
|
+
return {
|
|
40
|
+
"schema": UPLOAD_SCHEMA,
|
|
41
|
+
"bundle_schema": manifest.get("schema"),
|
|
42
|
+
"privacy_mode": manifest.get("privacy_mode", ""),
|
|
43
|
+
"upload_mode": "reports_included" if include_reports else "metadata_only",
|
|
44
|
+
"repo_slug": manifest.get("repo_slug", ""),
|
|
45
|
+
"team_id": manifest.get("team_id", ""),
|
|
46
|
+
"install_id": manifest.get("install_id", ""),
|
|
47
|
+
"excluded_content": manifest.get("excluded_content", []),
|
|
48
|
+
"reports": included_report_payloads(
|
|
49
|
+
manifest,
|
|
50
|
+
bundle_dir,
|
|
51
|
+
include_reports=include_reports,
|
|
52
|
+
),
|
|
53
|
+
"events": events,
|
|
54
|
+
"notes": [
|
|
55
|
+
"This upload payload is built from an explicit local bundle.",
|
|
56
|
+
"Report contents are included only when --include-reports is set.",
|
|
57
|
+
],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def post_upload_payload(
|
|
62
|
+
*,
|
|
63
|
+
payload: dict[str, Any],
|
|
64
|
+
endpoint: str,
|
|
65
|
+
token: str = "",
|
|
66
|
+
timeout: float = 20.0,
|
|
67
|
+
) -> dict[str, Any]:
|
|
68
|
+
_validate_upload_endpoint(endpoint)
|
|
69
|
+
body = json.dumps(payload, sort_keys=True).encode("utf-8")
|
|
70
|
+
headers = {
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
"User-Agent": "code-mower-cloud-upload",
|
|
73
|
+
}
|
|
74
|
+
if token:
|
|
75
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
76
|
+
request = urllib.request.Request(endpoint, data=body, headers=headers, method="POST")
|
|
77
|
+
try:
|
|
78
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
79
|
+
response_body = response.read().decode("utf-8")
|
|
80
|
+
status = response.getcode()
|
|
81
|
+
except urllib.error.HTTPError as exc:
|
|
82
|
+
detail = exc.read().decode("utf-8", errors="replace")
|
|
83
|
+
raise CloudBundleError(f"upload failed with HTTP {exc.code}: {detail}") from exc
|
|
84
|
+
except urllib.error.URLError as exc:
|
|
85
|
+
raise CloudBundleError(f"upload failed: {exc.reason}") from exc
|
|
86
|
+
try:
|
|
87
|
+
parsed = json.loads(response_body) if response_body else {}
|
|
88
|
+
except json.JSONDecodeError as exc:
|
|
89
|
+
raise CloudBundleError(f"upload response was not JSON: {exc}") from exc
|
|
90
|
+
if not isinstance(parsed, dict):
|
|
91
|
+
raise CloudBundleError("upload response JSON must be an object")
|
|
92
|
+
return {
|
|
93
|
+
"mode": "cloud-upload",
|
|
94
|
+
"endpoint": endpoint,
|
|
95
|
+
"status": status,
|
|
96
|
+
"response": parsed,
|
|
97
|
+
}
|