open-code-review-toolkit 0.1.0__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.
- ocr_toolkit/__init__.py +1 -0
- ocr_toolkit/_version.py +24 -0
- ocr_toolkit/cli.py +56 -0
- ocr_toolkit/common/__init__.py +1 -0
- ocr_toolkit/common/language.py +60 -0
- ocr_toolkit/common/markdown.py +214 -0
- ocr_toolkit/common/redaction.py +324 -0
- ocr_toolkit/config_writer.py +106 -0
- ocr_toolkit/configure.py +194 -0
- ocr_toolkit/context/__init__.py +1 -0
- ocr_toolkit/context/__main__.py +8 -0
- ocr_toolkit/context/ansible.py +561 -0
- ocr_toolkit/context/categorize.py +162 -0
- ocr_toolkit/context/instructions.py +269 -0
- ocr_toolkit/context/manifests.py +459 -0
- ocr_toolkit/context/planner.py +227 -0
- ocr_toolkit/context/render.py +955 -0
- ocr_toolkit/context/repo.py +672 -0
- ocr_toolkit/context/settings.py +97 -0
- ocr_toolkit/mcp_config.py +257 -0
- ocr_toolkit/posting/__init__.py +1 -0
- ocr_toolkit/posting/__main__.py +8 -0
- ocr_toolkit/posting/comments.py +87 -0
- ocr_toolkit/posting/formatting.py +755 -0
- ocr_toolkit/posting/gitlab.py +853 -0
- ocr_toolkit/posting/markers.py +284 -0
- ocr_toolkit/posting/payloads.py +141 -0
- ocr_toolkit/posting/result.py +116 -0
- ocr_toolkit/posting/settings.py +181 -0
- ocr_toolkit/posting/snapshot.py +468 -0
- ocr_toolkit/posting/workflow.py +873 -0
- ocr_toolkit/preflight.py +395 -0
- ocr_toolkit/py.typed +1 -0
- open_code_review_toolkit-0.1.0.dist-info/METADATA +283 -0
- open_code_review_toolkit-0.1.0.dist-info/RECORD +38 -0
- open_code_review_toolkit-0.1.0.dist-info/WHEEL +4 -0
- open_code_review_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
- open_code_review_toolkit-0.1.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Write Open Code Review configuration without exposing secrets in argv."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import stat
|
|
8
|
+
import tempfile
|
|
9
|
+
from collections.abc import Mapping
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class OCRConfigError(Exception):
|
|
15
|
+
"""OCR configuration could not be read or written safely."""
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def ocr_config_path() -> Path:
|
|
19
|
+
"""Return the OCR config path for the current HOME."""
|
|
20
|
+
|
|
21
|
+
return Path.home() / ".opencodereview" / "config.json"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def read_ocr_config(path: Path | None = None) -> dict[str, Any]:
|
|
25
|
+
"""Read OCR config JSON, returning an empty config when it is absent."""
|
|
26
|
+
|
|
27
|
+
config_path = path or ocr_config_path()
|
|
28
|
+
if not config_path.exists():
|
|
29
|
+
return {}
|
|
30
|
+
try:
|
|
31
|
+
data = json.loads(config_path.read_text(encoding="utf-8"))
|
|
32
|
+
except json.JSONDecodeError as exc:
|
|
33
|
+
raise OCRConfigError(f"OCR config is not valid JSON: {exc}") from exc
|
|
34
|
+
except UnicodeDecodeError as exc:
|
|
35
|
+
raise OCRConfigError(f"OCR config is not valid UTF-8: {exc}") from exc
|
|
36
|
+
except OSError as exc:
|
|
37
|
+
raise OCRConfigError(f"cannot read OCR config: {exc}") from exc
|
|
38
|
+
if not isinstance(data, dict):
|
|
39
|
+
raise OCRConfigError("OCR config top-level value is not an object")
|
|
40
|
+
return data
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def write_ocr_config(config: Mapping[str, Any], path: Path | None = None) -> None:
|
|
44
|
+
"""Atomically write OCR config with owner-only permissions."""
|
|
45
|
+
|
|
46
|
+
config_path = path or ocr_config_path()
|
|
47
|
+
try:
|
|
48
|
+
config_path.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
|
|
49
|
+
os.chmod(config_path.parent, 0o700)
|
|
50
|
+
except OSError as exc:
|
|
51
|
+
raise OCRConfigError(f"cannot prepare OCR config directory: {exc}") from exc
|
|
52
|
+
|
|
53
|
+
payload = json.dumps(config, ensure_ascii=False, indent=4, sort_keys=True) + "\n"
|
|
54
|
+
tmp_name = ""
|
|
55
|
+
try:
|
|
56
|
+
with tempfile.NamedTemporaryFile(
|
|
57
|
+
"w",
|
|
58
|
+
encoding="utf-8",
|
|
59
|
+
dir=config_path.parent,
|
|
60
|
+
prefix="config.",
|
|
61
|
+
suffix=".tmp",
|
|
62
|
+
delete=False,
|
|
63
|
+
) as tmp_file:
|
|
64
|
+
tmp_name = tmp_file.name
|
|
65
|
+
os.chmod(tmp_name, stat.S_IRUSR | stat.S_IWUSR)
|
|
66
|
+
tmp_file.write(payload)
|
|
67
|
+
tmp_file.flush()
|
|
68
|
+
os.fsync(tmp_file.fileno())
|
|
69
|
+
os.replace(tmp_name, config_path)
|
|
70
|
+
os.chmod(config_path, stat.S_IRUSR | stat.S_IWUSR)
|
|
71
|
+
except OSError as exc:
|
|
72
|
+
if tmp_name:
|
|
73
|
+
try:
|
|
74
|
+
Path(tmp_name).unlink(missing_ok=True)
|
|
75
|
+
except OSError:
|
|
76
|
+
pass
|
|
77
|
+
raise OCRConfigError(f"cannot write OCR config: {exc}") from exc
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def set_config_path(config: dict[str, Any], dotted_key: str, value: Any) -> None:
|
|
81
|
+
"""Set a dotted OCR config key in-place."""
|
|
82
|
+
|
|
83
|
+
parts = [part for part in dotted_key.split(".") if part]
|
|
84
|
+
if not parts:
|
|
85
|
+
raise OCRConfigError("empty OCR config key")
|
|
86
|
+
cursor: dict[str, Any] = config
|
|
87
|
+
for part in parts[:-1]:
|
|
88
|
+
existing = cursor.get(part)
|
|
89
|
+
if existing is None:
|
|
90
|
+
existing = {}
|
|
91
|
+
cursor[part] = existing
|
|
92
|
+
elif not isinstance(existing, dict):
|
|
93
|
+
raise OCRConfigError(
|
|
94
|
+
f"OCR config key {part!r} is not an object; refusing to overwrite it"
|
|
95
|
+
)
|
|
96
|
+
cursor = existing
|
|
97
|
+
cursor[parts[-1]] = value
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def update_ocr_config(values: Mapping[str, Any], path: Path | None = None) -> None:
|
|
101
|
+
"""Read, update, and write OCR config for dotted keys."""
|
|
102
|
+
|
|
103
|
+
config = read_ocr_config(path)
|
|
104
|
+
for key, value in values.items():
|
|
105
|
+
set_config_path(config, key, value)
|
|
106
|
+
write_ocr_config(config, path)
|
ocr_toolkit/configure.py
ADDED
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Configure OCR runtime settings from CI environment variables."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
from typing import Any
|
|
10
|
+
from urllib.parse import urlsplit
|
|
11
|
+
|
|
12
|
+
from ocr_toolkit.common.language import resolve_review_language
|
|
13
|
+
from ocr_toolkit.common.redaction import redact_sensitive
|
|
14
|
+
from ocr_toolkit.config_writer import OCRConfigError, update_ocr_config
|
|
15
|
+
|
|
16
|
+
HEADER_NAME_RE = re.compile(r"^[!#$%&'*+.^_`|~0-9A-Za-z-]+$")
|
|
17
|
+
LLM_PROTOCOLS = {"anthropic", "openai", "openai-responses"}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class OCRRuntimeConfigError(Exception):
|
|
21
|
+
"""OCR runtime config from CI env is invalid."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _env(name: str, default: str = "") -> str:
|
|
25
|
+
return os.environ.get(name, default).strip()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _bool_env(name: str) -> bool:
|
|
29
|
+
return _env(name).lower() == "true"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _required_env(name: str) -> str:
|
|
33
|
+
value = _env(name)
|
|
34
|
+
if not value:
|
|
35
|
+
raise OCRRuntimeConfigError(f"{name} is required")
|
|
36
|
+
return value
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _parse_extra_headers(value: str) -> dict[str, str]:
|
|
40
|
+
if not value:
|
|
41
|
+
return {}
|
|
42
|
+
try:
|
|
43
|
+
parsed = json.loads(value)
|
|
44
|
+
except json.JSONDecodeError as exc:
|
|
45
|
+
raise OCRRuntimeConfigError("OCR_LLM_EXTRA_HEADERS must be a JSON object") from exc
|
|
46
|
+
if not isinstance(parsed, dict):
|
|
47
|
+
raise OCRRuntimeConfigError("OCR_LLM_EXTRA_HEADERS must be a JSON object")
|
|
48
|
+
headers: dict[str, str] = {}
|
|
49
|
+
for key, raw_value in parsed.items():
|
|
50
|
+
if not isinstance(key, str) or not HEADER_NAME_RE.fullmatch(key):
|
|
51
|
+
raise OCRRuntimeConfigError(
|
|
52
|
+
"OCR_LLM_EXTRA_HEADERS contains an invalid HTTP header name"
|
|
53
|
+
)
|
|
54
|
+
if not isinstance(raw_value, str):
|
|
55
|
+
raise OCRRuntimeConfigError("OCR_LLM_EXTRA_HEADERS contains a non-string header value")
|
|
56
|
+
if "\n" in raw_value or "\r" in raw_value:
|
|
57
|
+
raise OCRRuntimeConfigError(
|
|
58
|
+
"OCR_LLM_EXTRA_HEADERS contains a header value with a line break"
|
|
59
|
+
)
|
|
60
|
+
headers[key] = raw_value
|
|
61
|
+
return headers
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _parse_extra_body(value: str) -> Any:
|
|
65
|
+
if not value:
|
|
66
|
+
return None
|
|
67
|
+
try:
|
|
68
|
+
parsed = json.loads(value)
|
|
69
|
+
except json.JSONDecodeError as exc:
|
|
70
|
+
raise OCRRuntimeConfigError("OCR_LLM_EXTRA_BODY must be valid JSON") from exc
|
|
71
|
+
if not isinstance(parsed, dict):
|
|
72
|
+
raise OCRRuntimeConfigError("OCR_LLM_EXTRA_BODY must be a JSON object")
|
|
73
|
+
return parsed
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _llm_protocol() -> str:
|
|
77
|
+
"""Resolve and validate the OCR 1.7.10 LLM wire protocol."""
|
|
78
|
+
|
|
79
|
+
protocol = _env("OCR_LLM_PROTOCOL")
|
|
80
|
+
legacy_mode = _env("OCR_USE_ANTHROPIC").lower()
|
|
81
|
+
if legacy_mode not in {"", "false", "true"}:
|
|
82
|
+
raise OCRRuntimeConfigError("OCR_USE_ANTHROPIC must be true or false when set")
|
|
83
|
+
if not protocol:
|
|
84
|
+
protocol = "anthropic" if legacy_mode == "true" else "openai"
|
|
85
|
+
if protocol not in LLM_PROTOCOLS:
|
|
86
|
+
allowed = ", ".join(sorted(LLM_PROTOCOLS))
|
|
87
|
+
raise OCRRuntimeConfigError(f"OCR_LLM_PROTOCOL must be one of: {allowed}")
|
|
88
|
+
|
|
89
|
+
if legacy_mode == "true" and protocol != "anthropic":
|
|
90
|
+
raise OCRRuntimeConfigError("OCR_LLM_PROTOCOL conflicts with OCR_USE_ANTHROPIC")
|
|
91
|
+
if legacy_mode == "false" and protocol == "anthropic":
|
|
92
|
+
raise OCRRuntimeConfigError("OCR_LLM_PROTOCOL conflicts with OCR_USE_ANTHROPIC")
|
|
93
|
+
return protocol
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _llm_extra_body(protocol: str) -> dict[str, Any] | None:
|
|
97
|
+
"""Return explicit OCR LLM extra body with safe Anthropic defaults merged."""
|
|
98
|
+
|
|
99
|
+
raw_env = os.environ.get("OCR_LLM_EXTRA_BODY")
|
|
100
|
+
raw = raw_env.strip() if raw_env is not None else ""
|
|
101
|
+
explicit_object = bool(raw)
|
|
102
|
+
extra_body = _parse_extra_body(raw)
|
|
103
|
+
if extra_body is None:
|
|
104
|
+
extra_body = {}
|
|
105
|
+
|
|
106
|
+
if protocol == "anthropic" and _bool_env("OCR_ANTHROPIC_DISABLE_THINKING"):
|
|
107
|
+
existing = extra_body.get("thinking")
|
|
108
|
+
disabled = {"type": "disabled"}
|
|
109
|
+
if existing is not None and existing != disabled:
|
|
110
|
+
raise OCRRuntimeConfigError(
|
|
111
|
+
"OCR_ANTHROPIC_DISABLE_THINKING conflicts with OCR_LLM_EXTRA_BODY.thinking"
|
|
112
|
+
)
|
|
113
|
+
extra_body["thinking"] = disabled
|
|
114
|
+
|
|
115
|
+
return extra_body if explicit_object or extra_body else None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def build_config_updates() -> dict[str, Any]:
|
|
119
|
+
"""Build OCR config updates from already-normalized CI environment."""
|
|
120
|
+
|
|
121
|
+
review_language = resolve_review_language()
|
|
122
|
+
llm_url = _required_env("OCR_LLM_URL")
|
|
123
|
+
llm_token = _required_env("OCR_LLM_TOKEN")
|
|
124
|
+
try:
|
|
125
|
+
parsed_llm_url = urlsplit(llm_url)
|
|
126
|
+
parsed_llm_port = parsed_llm_url.port
|
|
127
|
+
parsed_llm_hostname = parsed_llm_url.hostname
|
|
128
|
+
parsed_llm_username = parsed_llm_url.username
|
|
129
|
+
parsed_llm_password = parsed_llm_url.password
|
|
130
|
+
except ValueError as exc:
|
|
131
|
+
raise OCRRuntimeConfigError(
|
|
132
|
+
"OCR_LLM_URL must be an absolute HTTPS URL without embedded credentials"
|
|
133
|
+
) from exc
|
|
134
|
+
if (
|
|
135
|
+
parsed_llm_url.scheme.lower() != "https"
|
|
136
|
+
or not parsed_llm_hostname
|
|
137
|
+
or (parsed_llm_port is None and parsed_llm_url.netloc.endswith(":"))
|
|
138
|
+
or parsed_llm_username is not None
|
|
139
|
+
or parsed_llm_password is not None
|
|
140
|
+
):
|
|
141
|
+
raise OCRRuntimeConfigError(
|
|
142
|
+
"OCR_LLM_URL must be an absolute HTTPS URL without embedded credentials"
|
|
143
|
+
)
|
|
144
|
+
llm_model = _required_env("OCR_LLM_MODEL")
|
|
145
|
+
llm_protocol = _llm_protocol()
|
|
146
|
+
auth_header = _env("OCR_LLM_AUTH_HEADER", "Authorization") or "Authorization"
|
|
147
|
+
if not HEADER_NAME_RE.fullmatch(auth_header):
|
|
148
|
+
raise OCRRuntimeConfigError("OCR_LLM_AUTH_HEADER is not a valid HTTP header name")
|
|
149
|
+
|
|
150
|
+
updates: dict[str, Any] = {
|
|
151
|
+
"language": review_language,
|
|
152
|
+
"llm.url": llm_url,
|
|
153
|
+
"llm.auth_token": llm_token,
|
|
154
|
+
"llm.model": llm_model,
|
|
155
|
+
"llm.protocol": llm_protocol,
|
|
156
|
+
"llm.use_anthropic": llm_protocol == "anthropic",
|
|
157
|
+
"llm.auth_header": auth_header,
|
|
158
|
+
"telemetry.enabled": _bool_env("OCR_TELEMETRY_ENABLED"),
|
|
159
|
+
"telemetry.content_logging": _bool_env("OCR_TELEMETRY_CONTENT_LOGGING"),
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
extra_headers = _parse_extra_headers(_env("OCR_LLM_EXTRA_HEADERS"))
|
|
163
|
+
if any(header.casefold() == auth_header.casefold() for header in extra_headers):
|
|
164
|
+
raise OCRRuntimeConfigError("OCR_LLM_EXTRA_HEADERS must not duplicate OCR_LLM_AUTH_HEADER")
|
|
165
|
+
if extra_headers:
|
|
166
|
+
updates["llm.extra_headers"] = extra_headers
|
|
167
|
+
|
|
168
|
+
extra_body = _llm_extra_body(llm_protocol)
|
|
169
|
+
if extra_body is not None:
|
|
170
|
+
updates["llm.extra_body"] = extra_body
|
|
171
|
+
|
|
172
|
+
if _bool_env("OCR_TELEMETRY_ENABLED"):
|
|
173
|
+
updates["telemetry.exporter"] = _env("OCR_TELEMETRY_EXPORTER")
|
|
174
|
+
otlp_endpoint = _env("OCR_TELEMETRY_OTLP_ENDPOINT")
|
|
175
|
+
if otlp_endpoint:
|
|
176
|
+
updates["telemetry.otlp_endpoint"] = otlp_endpoint
|
|
177
|
+
|
|
178
|
+
return updates
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def main() -> int:
|
|
182
|
+
"""Write OCR config and return a process exit code."""
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
update_ocr_config(build_config_updates())
|
|
186
|
+
except (OCRRuntimeConfigError, OCRConfigError) as exc:
|
|
187
|
+
print(f"Failed to configure OCR runtime: {redact_sensitive(str(exc))}", file=sys.stderr)
|
|
188
|
+
return 1
|
|
189
|
+
print("OCR runtime config written")
|
|
190
|
+
return 0
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
if __name__ == "__main__":
|
|
194
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Open Code Review CI helper package."""
|