gateflow 0.1.0a2__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.
- gateflow/__init__.py +3 -0
- gateflow/__main__.py +3 -0
- gateflow/api_shim.py +57 -0
- gateflow/cli.py +226 -0
- gateflow/config.py +55 -0
- gateflow/import_luvatrix.py +358 -0
- gateflow/io.py +16 -0
- gateflow/policy.py +55 -0
- gateflow/render.py +138 -0
- gateflow/resources.py +159 -0
- gateflow/scaffold.py +147 -0
- gateflow/validate.py +108 -0
- gateflow/workspace.py +53 -0
- gateflow-0.1.0a2.dist-info/METADATA +58 -0
- gateflow-0.1.0a2.dist-info/RECORD +18 -0
- gateflow-0.1.0a2.dist-info/WHEEL +5 -0
- gateflow-0.1.0a2.dist-info/entry_points.txt +2 -0
- gateflow-0.1.0a2.dist-info/top_level.txt +1 -0
gateflow/__init__.py
ADDED
gateflow/__main__.py
ADDED
gateflow/api_shim.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from gateflow.resources import create_resource, delete_resource, get_resource, list_resource, update_resource
|
|
8
|
+
from gateflow.workspace import GateflowWorkspace
|
|
9
|
+
|
|
10
|
+
VALID_RESOURCES = {"milestones", "tasks", "boards", "frameworks", "backlog"}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def execute_api(method: str, endpoint: str, *, body: str | None, root: Path) -> dict[str, Any]:
|
|
14
|
+
method = method.upper()
|
|
15
|
+
resource, item_id = _parse_endpoint(endpoint)
|
|
16
|
+
workspace = GateflowWorkspace(root)
|
|
17
|
+
|
|
18
|
+
result: Any
|
|
19
|
+
if method == "GET":
|
|
20
|
+
result = list_resource(workspace, resource) if item_id is None else get_resource(workspace, resource, item_id)
|
|
21
|
+
elif method == "POST":
|
|
22
|
+
if body is None:
|
|
23
|
+
raise ValueError("POST requires --body")
|
|
24
|
+
result = create_resource(workspace, resource, json.loads(body))
|
|
25
|
+
elif method == "PATCH":
|
|
26
|
+
if body is None:
|
|
27
|
+
raise ValueError("PATCH requires --body")
|
|
28
|
+
if item_id is None:
|
|
29
|
+
raise ValueError("PATCH requires resource id in endpoint")
|
|
30
|
+
result = update_resource(workspace, resource, item_id, json.loads(body))
|
|
31
|
+
elif method == "DELETE":
|
|
32
|
+
if item_id is None:
|
|
33
|
+
raise ValueError("DELETE requires resource id in endpoint")
|
|
34
|
+
result = delete_resource(workspace, resource, item_id)
|
|
35
|
+
else:
|
|
36
|
+
raise ValueError(f"unsupported method: {method}")
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
"compatibility_mode": "planning_api_shim_v1",
|
|
40
|
+
"method": method,
|
|
41
|
+
"path": endpoint,
|
|
42
|
+
"result": result,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _parse_endpoint(endpoint: str) -> tuple[str, str | None]:
|
|
47
|
+
if not endpoint.startswith("/"):
|
|
48
|
+
raise ValueError("endpoint must start with /")
|
|
49
|
+
parts = [part for part in endpoint.split("/") if part]
|
|
50
|
+
if not parts:
|
|
51
|
+
raise ValueError("endpoint must include resource")
|
|
52
|
+
resource = parts[0]
|
|
53
|
+
if resource not in VALID_RESOURCES:
|
|
54
|
+
raise ValueError(f"unsupported resource: {resource}")
|
|
55
|
+
if len(parts) == 1:
|
|
56
|
+
return resource, None
|
|
57
|
+
return resource, parts[1]
|
gateflow/cli.py
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from gateflow.api_shim import execute_api
|
|
9
|
+
from gateflow.config import get_config_value, set_config_value, show_config
|
|
10
|
+
from gateflow.import_luvatrix import check_luvatrix_import_drift, import_luvatrix
|
|
11
|
+
from gateflow.policy import PolicyViolation, enforce_protected_branch_write_guard
|
|
12
|
+
from gateflow.render import render_board, render_gantt
|
|
13
|
+
from gateflow.scaffold import doctor_workspace, scaffold_workspace
|
|
14
|
+
from gateflow.resources import ResourceError, create_resource, delete_resource, get_resource, list_resource, update_resource
|
|
15
|
+
from gateflow.validate import ValidationCommandError, run_validation
|
|
16
|
+
from gateflow.workspace import GateflowWorkspace
|
|
17
|
+
|
|
18
|
+
RESOURCES = ("milestones", "tasks", "boards", "frameworks", "backlog")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
22
|
+
parser = argparse.ArgumentParser(prog="gateflow")
|
|
23
|
+
parser.add_argument("--root", type=Path, default=Path.cwd())
|
|
24
|
+
parser.add_argument("--json-errors", action="store_true", help="Emit machine-readable error payloads.")
|
|
25
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
26
|
+
|
|
27
|
+
init_p = sub.add_parser("init")
|
|
28
|
+
init_sub = init_p.add_subparsers(dest="init_action", required=True)
|
|
29
|
+
scaffold_p = init_sub.add_parser("scaffold")
|
|
30
|
+
scaffold_p.add_argument("--profile", choices=["minimal", "discord", "enterprise"], default="minimal")
|
|
31
|
+
init_sub.add_parser("doctor")
|
|
32
|
+
|
|
33
|
+
config_p = sub.add_parser("config")
|
|
34
|
+
config_sub = config_p.add_subparsers(dest="config_action", required=True)
|
|
35
|
+
config_get = config_sub.add_parser("get")
|
|
36
|
+
config_get.add_argument("key")
|
|
37
|
+
config_set = config_sub.add_parser("set")
|
|
38
|
+
config_set.add_argument("key")
|
|
39
|
+
config_set.add_argument("value")
|
|
40
|
+
config_sub.add_parser("show")
|
|
41
|
+
|
|
42
|
+
validate_p = sub.add_parser("validate")
|
|
43
|
+
validate_sub = validate_p.add_subparsers(dest="validate_action", required=True)
|
|
44
|
+
validate_sub.add_parser("links")
|
|
45
|
+
validate_sub.add_parser("closeout")
|
|
46
|
+
validate_sub.add_parser("all")
|
|
47
|
+
|
|
48
|
+
api_p = sub.add_parser("api")
|
|
49
|
+
api_p.add_argument("verb_or_method")
|
|
50
|
+
api_p.add_argument("path", nargs="?")
|
|
51
|
+
api_p.add_argument("--body")
|
|
52
|
+
|
|
53
|
+
render_p = sub.add_parser("render")
|
|
54
|
+
render_sub = render_p.add_subparsers(dest="render_action", required=True)
|
|
55
|
+
gantt_p = render_sub.add_parser("gantt")
|
|
56
|
+
gantt_p.add_argument("--format", choices=["md", "ascii"])
|
|
57
|
+
gantt_p.add_argument("--out", type=Path)
|
|
58
|
+
board_p = render_sub.add_parser("board")
|
|
59
|
+
board_p.add_argument("--format", choices=["md", "ascii"])
|
|
60
|
+
board_p.add_argument("--out", type=Path)
|
|
61
|
+
|
|
62
|
+
for resource in RESOURCES:
|
|
63
|
+
rs = sub.add_parser(resource)
|
|
64
|
+
rsub = rs.add_subparsers(dest="action", required=True)
|
|
65
|
+
|
|
66
|
+
rsub.add_parser("list")
|
|
67
|
+
|
|
68
|
+
get_p = rsub.add_parser("get")
|
|
69
|
+
get_p.add_argument("item_id")
|
|
70
|
+
|
|
71
|
+
create_p = rsub.add_parser("create")
|
|
72
|
+
create_p.add_argument("--body", required=True)
|
|
73
|
+
|
|
74
|
+
update_p = rsub.add_parser("update")
|
|
75
|
+
update_p.add_argument("item_id")
|
|
76
|
+
update_p.add_argument("--body", required=True)
|
|
77
|
+
|
|
78
|
+
delete_p = rsub.add_parser("delete")
|
|
79
|
+
delete_p.add_argument("item_id")
|
|
80
|
+
|
|
81
|
+
import_p = sub.add_parser("import-luvatrix")
|
|
82
|
+
import_p.add_argument("--path", type=Path, required=True, help="Path to the Luvatrix repository root.")
|
|
83
|
+
import_p.add_argument("--check", action="store_true", help="Only check drift; do not write .gateflow files.")
|
|
84
|
+
|
|
85
|
+
return parser
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def main(argv: list[str] | None = None) -> int:
|
|
89
|
+
parser = build_parser()
|
|
90
|
+
args = parser.parse_args(argv)
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
return _dispatch(args)
|
|
94
|
+
except ValidationCommandError as exc:
|
|
95
|
+
_emit_error(json_mode=args.json_errors, error_type="validation", exit_code=2, message=str(exc), errors=exc.errors)
|
|
96
|
+
return 2
|
|
97
|
+
except PolicyViolation as exc:
|
|
98
|
+
_emit_error(json_mode=args.json_errors, error_type="policy", exit_code=3, message=str(exc), errors=[str(exc)])
|
|
99
|
+
return 3
|
|
100
|
+
except (ResourceError, FileNotFoundError, json.JSONDecodeError, ValueError) as exc:
|
|
101
|
+
_emit_error(json_mode=args.json_errors, error_type="validation", exit_code=2, message=str(exc), errors=[str(exc)])
|
|
102
|
+
return 2
|
|
103
|
+
except Exception as exc: # pragma: no cover - defensive contract
|
|
104
|
+
_emit_error(json_mode=args.json_errors, error_type="internal", exit_code=4, message=str(exc), errors=[str(exc)])
|
|
105
|
+
return 4
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _dispatch(args: argparse.Namespace) -> int:
|
|
109
|
+
if args.command == "init":
|
|
110
|
+
if args.init_action == "scaffold":
|
|
111
|
+
created = scaffold_workspace(root=args.root, profile=args.profile)
|
|
112
|
+
print(json.dumps({"status": "ok", "created": created}, indent=2, sort_keys=True))
|
|
113
|
+
return 0
|
|
114
|
+
if args.init_action == "doctor":
|
|
115
|
+
print(json.dumps(doctor_workspace(root=args.root), indent=2, sort_keys=True))
|
|
116
|
+
return 0
|
|
117
|
+
raise ValueError(f"unsupported init action: {args.init_action}")
|
|
118
|
+
|
|
119
|
+
if args.command == "config":
|
|
120
|
+
if args.config_action == "get":
|
|
121
|
+
print(json.dumps(get_config_value(args.root, args.key), indent=2, sort_keys=True))
|
|
122
|
+
return 0
|
|
123
|
+
if args.config_action == "set":
|
|
124
|
+
enforce_protected_branch_write_guard(args.root)
|
|
125
|
+
print(set_config_value(args.root, args.key, args.value))
|
|
126
|
+
return 0
|
|
127
|
+
if args.config_action == "show":
|
|
128
|
+
print(json.dumps(show_config(args.root), indent=2, sort_keys=True))
|
|
129
|
+
return 0
|
|
130
|
+
raise ValueError(f"unsupported config action: {args.config_action}")
|
|
131
|
+
|
|
132
|
+
if args.command == "api":
|
|
133
|
+
method, endpoint = _resolve_api_method_and_path(args.verb_or_method, args.path)
|
|
134
|
+
if method in {"POST", "PATCH", "DELETE"}:
|
|
135
|
+
enforce_protected_branch_write_guard(args.root)
|
|
136
|
+
print(json.dumps(execute_api(method, endpoint, body=args.body, root=args.root), indent=2, sort_keys=True))
|
|
137
|
+
return 0
|
|
138
|
+
|
|
139
|
+
if args.command == "validate":
|
|
140
|
+
ok, errors = run_validation(args.root, args.validate_action)
|
|
141
|
+
if ok:
|
|
142
|
+
print(f"validation: PASS ({args.validate_action})")
|
|
143
|
+
return 0
|
|
144
|
+
raise ValidationCommandError(args.validate_action, errors)
|
|
145
|
+
|
|
146
|
+
if args.command == "render":
|
|
147
|
+
workspace = GateflowWorkspace(args.root)
|
|
148
|
+
if args.render_action == "gantt":
|
|
149
|
+
output = render_gantt(workspace, out_path=args.out, fmt=args.format)
|
|
150
|
+
if args.out is None:
|
|
151
|
+
print(output, end="")
|
|
152
|
+
return 0
|
|
153
|
+
if args.render_action == "board":
|
|
154
|
+
output = render_board(workspace, out_path=args.out, fmt=args.format)
|
|
155
|
+
if args.out is None:
|
|
156
|
+
print(output, end="")
|
|
157
|
+
return 0
|
|
158
|
+
raise ValueError(f"unsupported render action: {args.render_action}")
|
|
159
|
+
|
|
160
|
+
if args.command == "import-luvatrix":
|
|
161
|
+
if args.check:
|
|
162
|
+
report = check_luvatrix_import_drift(args.path)
|
|
163
|
+
print(json.dumps(report.as_dict(), indent=2, sort_keys=True))
|
|
164
|
+
return 0 if len(report.findings) == 0 else 2
|
|
165
|
+
result = import_luvatrix(args.path)
|
|
166
|
+
print(json.dumps({"status": "ok", **result.as_dict()}, indent=2, sort_keys=True))
|
|
167
|
+
return 0
|
|
168
|
+
|
|
169
|
+
workspace = GateflowWorkspace(args.root)
|
|
170
|
+
resource = args.command
|
|
171
|
+
action = args.action
|
|
172
|
+
|
|
173
|
+
if action == "list":
|
|
174
|
+
print(json.dumps(list_resource(workspace, resource), indent=2, sort_keys=True))
|
|
175
|
+
return 0
|
|
176
|
+
if action == "get":
|
|
177
|
+
print(json.dumps(get_resource(workspace, resource, args.item_id), indent=2, sort_keys=True))
|
|
178
|
+
return 0
|
|
179
|
+
if action == "create":
|
|
180
|
+
enforce_protected_branch_write_guard(args.root)
|
|
181
|
+
print(create_resource(workspace, resource, json.loads(args.body)))
|
|
182
|
+
return 0
|
|
183
|
+
if action == "update":
|
|
184
|
+
enforce_protected_branch_write_guard(args.root)
|
|
185
|
+
print(update_resource(workspace, resource, args.item_id, json.loads(args.body)))
|
|
186
|
+
return 0
|
|
187
|
+
if action == "delete":
|
|
188
|
+
enforce_protected_branch_write_guard(args.root)
|
|
189
|
+
print(delete_resource(workspace, resource, args.item_id))
|
|
190
|
+
return 0
|
|
191
|
+
raise ValueError(f"unsupported action: {action}")
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _resolve_api_method_and_path(verb_or_method: str, path: str | None) -> tuple[str, str]:
|
|
195
|
+
method = verb_or_method.upper()
|
|
196
|
+
if path is None:
|
|
197
|
+
raise ValueError("api requires METHOD and /resource path")
|
|
198
|
+
if method in {"GET", "POST", "PATCH", "DELETE"}:
|
|
199
|
+
return method, path
|
|
200
|
+
raise ValueError(f"unsupported api method: {verb_or_method}")
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _emit_error(
|
|
204
|
+
*,
|
|
205
|
+
json_mode: bool,
|
|
206
|
+
error_type: str,
|
|
207
|
+
exit_code: int,
|
|
208
|
+
message: str,
|
|
209
|
+
errors: list[str],
|
|
210
|
+
) -> None:
|
|
211
|
+
if json_mode:
|
|
212
|
+
payload: dict[str, Any] = {
|
|
213
|
+
"ok": False,
|
|
214
|
+
"error_type": error_type,
|
|
215
|
+
"exit_code": exit_code,
|
|
216
|
+
"message": message,
|
|
217
|
+
"errors": errors,
|
|
218
|
+
}
|
|
219
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
220
|
+
return
|
|
221
|
+
prefix = "internal error" if error_type == "internal" else f"{error_type} error"
|
|
222
|
+
print(f"{prefix}: {message}")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__": # pragma: no cover
|
|
226
|
+
raise SystemExit(main())
|
gateflow/config.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from gateflow.io import read_json, write_json
|
|
8
|
+
|
|
9
|
+
_ALLOWED_PREFIXES = ("policy.protected_branches", "defaults", "render")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def config_path(root: Path) -> Path:
|
|
13
|
+
return root / ".gateflow" / "config.json"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def show_config(root: Path) -> dict[str, Any]:
|
|
17
|
+
return read_json(config_path(root))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_config_value(root: Path, key: str) -> Any:
|
|
21
|
+
node: Any = show_config(root)
|
|
22
|
+
for part in key.split("."):
|
|
23
|
+
if not isinstance(node, dict) or part not in node:
|
|
24
|
+
raise ValueError(f"unknown config key: {key}")
|
|
25
|
+
node = node[part]
|
|
26
|
+
return node
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def set_config_value(root: Path, key: str, value_raw: str) -> str:
|
|
30
|
+
if not any(key == allowed or key.startswith(f"{allowed}.") for allowed in _ALLOWED_PREFIXES):
|
|
31
|
+
raise ValueError("config key must target policy.protected_branches, defaults, or render")
|
|
32
|
+
|
|
33
|
+
data = show_config(root)
|
|
34
|
+
value = _coerce_value(value_raw)
|
|
35
|
+
|
|
36
|
+
parts = key.split(".")
|
|
37
|
+
node = data
|
|
38
|
+
for part in parts[:-1]:
|
|
39
|
+
child = node.get(part)
|
|
40
|
+
if child is None:
|
|
41
|
+
child = {}
|
|
42
|
+
node[part] = child
|
|
43
|
+
if not isinstance(child, dict):
|
|
44
|
+
raise ValueError(f"config path is not an object: {part}")
|
|
45
|
+
node = child
|
|
46
|
+
node[parts[-1]] = value
|
|
47
|
+
write_json(config_path(root), data)
|
|
48
|
+
return f"updated config {key}"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _coerce_value(value_raw: str) -> Any:
|
|
52
|
+
try:
|
|
53
|
+
return json.loads(value_raw)
|
|
54
|
+
except json.JSONDecodeError:
|
|
55
|
+
return value_raw
|