gitpro 0.3.1__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.
@@ -0,0 +1,117 @@
1
+ """Versioned, bounded persistent-change requests. Approval is deliberately not a request field."""
2
+ from __future__ import annotations
3
+
4
+ import os
5
+ import json
6
+ import re
7
+ from pathlib import Path
8
+
9
+ from . import config
10
+ from .approval_policy import AUTHORITY_SCHEMA
11
+ from .operation_contract import MAX_REQUEST, OperationError
12
+
13
+ KINDS = {
14
+ "configure": ("config_path", "toml"),
15
+ "regenerate": ("config_path",),
16
+ "repo-identity": ("config_path", "repo"),
17
+ "undo": ("change_id",),
18
+ }
19
+ REQUIRED = {"configure": ("toml",), "regenerate": (), "repo-identity": ("repo",), "undo": ("change_id",)}
20
+ STRING = {"type": "string", "minLength": 1, "maxLength": MAX_REQUEST}
21
+ REQUEST_SCHEMA = {
22
+ "type": "object", "additionalProperties": False, "required": ["version", "kind"],
23
+ "properties": {
24
+ "version": {"type": "integer", "enum": [1]}, "kind": {"type": "string", "enum": list(KINDS)},
25
+ "config_path": {**STRING, "description": "Absolute gitpro TOML path inside your home directory."},
26
+ "repo": {**STRING, "description": "Absolute worktree root; repo-identity only."},
27
+ "toml": {**STRING, "description": "Complete proposed configuration; values must be explicitly supplied."},
28
+ "change_id": {**STRING, "pattern": "^[0-9a-f]{64}$"},
29
+ },
30
+ "oneOf": [
31
+ {"properties": {"kind": {"const": kind}}, "required": list(REQUIRED[kind]),
32
+ "not": {"anyOf": [{"required": [k]} for k in
33
+ sorted(set(sum(KINDS.values(), ())) - set(fields))]}}
34
+ for kind, fields in KINDS.items()
35
+ ],
36
+ }
37
+ RESULT_SCHEMA = {
38
+ "type": "object", "additionalProperties": False,
39
+ "required": ["version", "command", "ok", "decision", "problems"],
40
+ "properties": {
41
+ "version": {"const": 1}, "command": {"enum": ["change propose", "change approve", "change apply",
42
+ "change status", "change recover"]},
43
+ "ok": {"type": "boolean"}, "decision": {"enum": ["ready", "blocked", "needs_input",
44
+ "needs_approval", "indeterminate"]},
45
+ "problems": {"type": "array", "items": {
46
+ "type": "object", "required": ["code", "message", "decision"]}},
47
+ "plan_id": {"type": "string", "pattern": "^[0-9a-f]{64}$"},
48
+ "status": {"enum": ["proposed", "no_changes", "approved", "applying", "applied", "rolled_back",
49
+ "indeterminate", "rejected", "stale"]},
50
+ "request": REQUEST_SCHEMA,
51
+ "changes": {"type": "array", "items": {
52
+ "type": "object", "required": ["path", "action", "before_sha256", "after_sha256", "diff"],
53
+ "properties": {"path": STRING, "action": {"enum": ["create", "update", "chmod", "remove"]},
54
+ "before_sha256": {"type": ["string", "null"]},
55
+ "after_sha256": {"type": ["string", "null"]},
56
+ "diff": {"type": "string"}, "before_mode": {"type": ["integer", "null"]},
57
+ "after_mode": {"type": ["integer", "null"]}}}},
58
+ "replayed": {"type": "boolean"}, "backup_store": {"type": "string"},
59
+ "files": {"type": "array", "items": {"type": "string"}},
60
+ "approval": {"type": "string"}, "verification": {"type": "string"},
61
+ "authority": AUTHORITY_SCHEMA,
62
+ },
63
+ }
64
+
65
+
66
+ def plan_id(value: object) -> str:
67
+ if not isinstance(value, str) or re.fullmatch(r"[0-9a-f]{64}", value) is None:
68
+ raise OperationError("invalid_plan", "Supply the complete 64-character plan ID.", "needs_input")
69
+ return value
70
+
71
+
72
+ def parse(raw: object) -> dict:
73
+ if not isinstance(raw, dict) or type(raw.get("version")) is not int or raw["version"] != 1:
74
+ raise OperationError("invalid_request", "Supply a version-1 change request.", "needs_input")
75
+ try:
76
+ size = len(json.dumps(raw, ensure_ascii=False).encode("utf-8"))
77
+ except (TypeError, ValueError, RecursionError):
78
+ raise OperationError("invalid_request", "Change requests must contain JSON values.", "needs_input") from None
79
+ if size > MAX_REQUEST:
80
+ raise OperationError("invalid_request", "Change requests are limited to 128 KiB.", "needs_input")
81
+ kind = raw.get("kind")
82
+ if not isinstance(kind, str) or kind not in KINDS:
83
+ raise OperationError("invalid_request", "Use configure, regenerate, repo-identity or undo.", "needs_input")
84
+ if set(raw) - {"version", "kind", *KINDS[kind]}:
85
+ raise OperationError("invalid_request", "Unknown or inapplicable change-request field.", "needs_input")
86
+ for key in REQUIRED[kind]:
87
+ if key not in raw:
88
+ raise OperationError("missing_input", f"Supply {key}; gitpro will not invent its value.", "needs_input")
89
+ out = dict(raw)
90
+ for key, value in raw.items():
91
+ if key in ("version", "kind"):
92
+ continue
93
+ if not isinstance(value, str) or not value.strip() or "\0" in value or len(value.encode()) > MAX_REQUEST:
94
+ raise OperationError("invalid_request", f"Invalid {key}.", "needs_input")
95
+ if kind == "undo":
96
+ out["change_id"] = plan_id(raw["change_id"])
97
+ return out
98
+ home = Path.home().resolve()
99
+ source = Path(raw.get("config_path") or os.environ.get(config.ENV_VAR) or home / config.USER_CONFIG)
100
+ if not source.is_absolute() or source.suffix != ".toml" or home not in source.parents:
101
+ raise OperationError("invalid_config_path", "Use an absolute .toml configuration path inside your home.",
102
+ "needs_input")
103
+ if ".." in source.parts or any(ord(c) < 32 or ord(c) == 127 for c in str(source)):
104
+ raise OperationError("invalid_config_path", "Use a normalized configuration path without control characters.",
105
+ "needs_input")
106
+ out["config_path"] = str(source)
107
+ if kind == "repo-identity":
108
+ repo = Path(raw["repo"])
109
+ if not repo.is_absolute() or any(ord(c) < 32 or ord(c) == 127 for c in str(repo)):
110
+ raise OperationError("invalid_repo", "Supply an absolute worktree root.", "needs_input")
111
+ out["repo"] = str(repo.resolve())
112
+ return out
113
+
114
+
115
+ def error(command: str, exc: OperationError) -> dict:
116
+ return {"version": 1, "command": "change " + command, "ok": False, "decision": exc.decision,
117
+ "problems": [exc.problem()]}