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.
Files changed (38) hide show
  1. ocr_toolkit/__init__.py +1 -0
  2. ocr_toolkit/_version.py +24 -0
  3. ocr_toolkit/cli.py +56 -0
  4. ocr_toolkit/common/__init__.py +1 -0
  5. ocr_toolkit/common/language.py +60 -0
  6. ocr_toolkit/common/markdown.py +214 -0
  7. ocr_toolkit/common/redaction.py +324 -0
  8. ocr_toolkit/config_writer.py +106 -0
  9. ocr_toolkit/configure.py +194 -0
  10. ocr_toolkit/context/__init__.py +1 -0
  11. ocr_toolkit/context/__main__.py +8 -0
  12. ocr_toolkit/context/ansible.py +561 -0
  13. ocr_toolkit/context/categorize.py +162 -0
  14. ocr_toolkit/context/instructions.py +269 -0
  15. ocr_toolkit/context/manifests.py +459 -0
  16. ocr_toolkit/context/planner.py +227 -0
  17. ocr_toolkit/context/render.py +955 -0
  18. ocr_toolkit/context/repo.py +672 -0
  19. ocr_toolkit/context/settings.py +97 -0
  20. ocr_toolkit/mcp_config.py +257 -0
  21. ocr_toolkit/posting/__init__.py +1 -0
  22. ocr_toolkit/posting/__main__.py +8 -0
  23. ocr_toolkit/posting/comments.py +87 -0
  24. ocr_toolkit/posting/formatting.py +755 -0
  25. ocr_toolkit/posting/gitlab.py +853 -0
  26. ocr_toolkit/posting/markers.py +284 -0
  27. ocr_toolkit/posting/payloads.py +141 -0
  28. ocr_toolkit/posting/result.py +116 -0
  29. ocr_toolkit/posting/settings.py +181 -0
  30. ocr_toolkit/posting/snapshot.py +468 -0
  31. ocr_toolkit/posting/workflow.py +873 -0
  32. ocr_toolkit/preflight.py +395 -0
  33. ocr_toolkit/py.typed +1 -0
  34. open_code_review_toolkit-0.1.0.dist-info/METADATA +283 -0
  35. open_code_review_toolkit-0.1.0.dist-info/RECORD +38 -0
  36. open_code_review_toolkit-0.1.0.dist-info/WHEEL +4 -0
  37. open_code_review_toolkit-0.1.0.dist-info/entry_points.txt +2 -0
  38. 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)
@@ -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."""
@@ -0,0 +1,8 @@
1
+ """CLI entrypoint for OCR review context generation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ocr_toolkit.context.render import main
6
+
7
+ if __name__ == "__main__":
8
+ raise SystemExit(main())