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 ADDED
@@ -0,0 +1,3 @@
1
+ __all__ = ["__version__"]
2
+
3
+ __version__ = "0.1.0a2"
gateflow/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from gateflow.cli import main
2
+
3
+ raise SystemExit(main())
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