arch-insight-cli 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.
arch_cli/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """ARCH-Insight command-line client."""
2
+
3
+ __version__ = "0.1.0"
arch_cli/main.py ADDED
@@ -0,0 +1,273 @@
1
+ """ARCH-Insight command-line client.
2
+
3
+ Product API commands use a GoTrue access token (ARCH_ACCESS_TOKEN). Developer
4
+ platform commands also accept the platform personal access token (ARCH_TOKEN).
5
+ Secrets are delegated to the separately maintained arch-secrets package.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ import argparse
10
+ import json
11
+ import os
12
+ import secrets
13
+ import sys
14
+ import urllib.error
15
+ import urllib.parse
16
+ import urllib.request
17
+ from pathlib import Path
18
+
19
+ DEFAULT_API = "https://arch-insight.com"
20
+ TIMEOUT = 60
21
+
22
+
23
+ class CliError(RuntimeError):
24
+ pass
25
+
26
+
27
+ def _token(args) -> str:
28
+ token = (args.token or os.environ.get("ARCH_ACCESS_TOKEN")
29
+ or os.environ.get("ARCH_TOKEN") or "")
30
+ if not token:
31
+ raise CliError(
32
+ "No access token. Set ARCH_ACCESS_TOKEN to a GoTrue session token; "
33
+ "developer-platform commands also accept ARCH_TOKEN.")
34
+ return token.strip()
35
+
36
+
37
+ def request(args, method: str, path: str, body=None, *, content_type="application/json"):
38
+ data = body
39
+ headers = {"Accept": "application/json", "User-Agent": f"arch-cli/{__import__('arch_cli').__version__}"}
40
+ if isinstance(body, (dict, list)):
41
+ data = json.dumps(body, ensure_ascii=False).encode("utf-8")
42
+ headers["Content-Type"] = "application/json"
43
+ elif isinstance(body, bytes):
44
+ headers["Content-Type"] = content_type
45
+ headers["Authorization"] = "Bearer " + _token(args)
46
+ url = args.api.rstrip("/") + path
47
+ req = urllib.request.Request(url, data=data, method=method, headers=headers)
48
+ try:
49
+ with urllib.request.urlopen(req, timeout=TIMEOUT) as response:
50
+ raw = response.read()
51
+ if not raw:
52
+ return {}
53
+ if "json" in response.headers.get("Content-Type", ""):
54
+ return json.loads(raw.decode("utf-8"))
55
+ return raw
56
+ except urllib.error.HTTPError as exc:
57
+ detail = exc.read().decode("utf-8", "replace")[:500]
58
+ if exc.code in (401, 403):
59
+ raise CliError("ARCH-Insight rejected the access token. Check its type, expiry, and account access.") from exc
60
+ raise CliError(f"{method} {path} returned HTTP {exc.code}: {detail}") from exc
61
+ except urllib.error.URLError as exc:
62
+ raise CliError(f"Could not reach {args.api}: {exc.reason}") from exc
63
+
64
+
65
+ def emit(value) -> None:
66
+ print(json.dumps(value, ensure_ascii=False, indent=2, default=str))
67
+
68
+
69
+ def task_create(args):
70
+ payload = {"title": args.title, "description": args.description,
71
+ "status": args.status, "priority": args.priority}
72
+ for field in ("project_id", "board_id", "factsheet_id", "due_date"):
73
+ value = getattr(args, field)
74
+ if value is not None:
75
+ payload[field] = value
76
+ if args.assignee:
77
+ payload["assignees"] = args.assignee
78
+ emit(request(args, "POST", "/api/agile/tasks", payload))
79
+
80
+
81
+ def task_list(args):
82
+ query = ("?" + urllib.parse.urlencode({"project_id": args.project_id})
83
+ if args.project_id is not None else "")
84
+ emit(request(args, "GET", "/api/agile/tasks" + query))
85
+
86
+
87
+ def factsheet_create(args):
88
+ payload = _read_json(args.file)
89
+ if not isinstance(payload, dict):
90
+ raise CliError("factsheet create expects one JSON object.")
91
+ emit(request(args, "POST", "/api/factsheets", payload))
92
+
93
+
94
+ def factsheet_list(args):
95
+ emit(request(args, "GET", "/api/factsheets"))
96
+
97
+
98
+ def _read_json(path: str):
99
+ try:
100
+ return json.loads(Path(path).read_text(encoding="utf-8"))
101
+ except OSError as exc:
102
+ raise CliError(f"Cannot read {path}: {exc}") from exc
103
+ except json.JSONDecodeError as exc:
104
+ raise CliError(f"Invalid JSON in {path}: {exc}") from exc
105
+
106
+
107
+ def _multipart_file(path: Path) -> tuple[bytes, str]:
108
+ boundary = "archcli-" + secrets.token_hex(16)
109
+ filename = path.name.replace('"', "_")
110
+ content = path.read_bytes()
111
+ head = (f"--{boundary}\r\n"
112
+ f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n'
113
+ "Content-Type: application/zip\r\n\r\n").encode("utf-8")
114
+ body = head + content + f"\r\n--{boundary}--\r\n".encode("ascii")
115
+ return body, f"multipart/form-data; boundary={boundary}"
116
+
117
+
118
+ def factsheet_import(args):
119
+ path = Path(args.file)
120
+ if not path.is_file():
121
+ raise CliError(f"File not found: {path}")
122
+ if path.suffix.lower() == ".zip":
123
+ body, content_type = _multipart_file(path)
124
+ emit(request(args, "POST", "/api/factsheets/import-zip", body,
125
+ content_type=content_type))
126
+ return
127
+ payload = _read_json(str(path))
128
+ rows = payload if isinstance(payload, list) else [payload]
129
+ if not rows or any(not isinstance(row, dict) for row in rows):
130
+ raise CliError("JSON import must contain one fact-sheet object or a list of objects.")
131
+ # The API validates and creates each fact sheet. If an item fails, report
132
+ # its index; earlier successful creates remain in the account.
133
+ created = []
134
+ for index, row in enumerate(rows, start=1):
135
+ try:
136
+ created.append(request(args, "POST", "/api/factsheets", row))
137
+ except CliError as exc:
138
+ raise CliError(f"Import stopped at item {index}/{len(rows)} after {len(created)} created: {exc}") from exc
139
+ emit({"imported": len(created), "items": created})
140
+
141
+
142
+ # Developer platform commands use the same API surface as devctl.
143
+ def repo_create(args):
144
+ if "/" not in args.path:
145
+ raise CliError("Repository path must be <namespace>/<name>.")
146
+ namespace, name = args.path.split("/", 1)
147
+ row = request(args, "POST", "/api/platform/repos",
148
+ {"namespace": namespace, "name": name, "description": args.description})
149
+ emit(row)
150
+
151
+
152
+ def repo_list(args):
153
+ emit(request(args, "GET", "/api/platform/repos"))
154
+
155
+
156
+ def pipeline_list(args):
157
+ query = ("?" + urllib.parse.urlencode({"repo_id": args.repo})
158
+ if args.repo is not None else "")
159
+ emit(request(args, "GET", "/api/platform/pipelines" + query))
160
+
161
+
162
+ def pipeline_show(args):
163
+ emit(request(args, "GET", f"/api/platform/pipelines/{args.pipeline_id}"))
164
+
165
+
166
+ def pipeline_cancel(args):
167
+ emit(request(args, "POST", f"/api/platform/pipelines/{args.pipeline_id}/cancel", {}))
168
+
169
+
170
+ def pipeline_logs(args):
171
+ rows = request(args, "GET", f"/api/platform/jobs/{args.job_id}/logs")
172
+ for row in rows:
173
+ sys.stdout.write(row.get("content") or "")
174
+
175
+
176
+ def registry_list(args):
177
+ emit(request(args, "GET", "/v2/_catalog"))
178
+
179
+
180
+ def registry_tags(args):
181
+ emit(request(args, "GET", f"/v2/{urllib.parse.quote(args.repository, safe='/')}/tags/list"))
182
+
183
+
184
+ def runner_list(args):
185
+ emit(request(args, "GET", "/api/platform/runners"))
186
+
187
+
188
+ def deploy_list(args):
189
+ query = ("?" + urllib.parse.urlencode({"environment": args.environment})
190
+ if args.environment else "")
191
+ emit(request(args, "GET", "/api/platform/deployments" + query))
192
+
193
+
194
+ def secrets_command(args):
195
+ from arch_secrets.main import main as secrets_main
196
+ forwarded = args.secrets_args
197
+ if forwarded and forwarded[0] == "--":
198
+ forwarded = forwarded[1:]
199
+ return secrets_main(forwarded)
200
+
201
+
202
+ def build_parser():
203
+ parser = argparse.ArgumentParser(prog="arch", description="ARCH-Insight from a terminal.")
204
+ parser.add_argument("--api", default=os.environ.get("ARCH_API_URL", DEFAULT_API),
205
+ help="ARCH-Insight base URL")
206
+ parser.add_argument("--token", help="GoTrue access token; prefer ARCH_ACCESS_TOKEN")
207
+ areas = parser.add_subparsers(dest="area", required=True)
208
+
209
+ task = areas.add_parser("task", help="Manage Agile tasks").add_subparsers(dest="action", required=True)
210
+ make_task = task.add_parser("create", help="Create a task")
211
+ make_task.add_argument("title")
212
+ make_task.add_argument("--description")
213
+ make_task.add_argument("--status", choices=("todo", "in_progress", "done"), default="todo")
214
+ make_task.add_argument("--priority", choices=("low", "medium", "high"), default="medium")
215
+ make_task.add_argument("--project-id", type=int)
216
+ make_task.add_argument("--board-id", type=int)
217
+ make_task.add_argument("--factsheet-id", type=int)
218
+ make_task.add_argument("--due-date")
219
+ make_task.add_argument("--assignee", action="append", help="Assignee name; repeat for multiple people")
220
+ make_task.set_defaults(func=task_create)
221
+ list_task = task.add_parser("list")
222
+ list_task.add_argument("--project-id", type=int)
223
+ list_task.set_defaults(func=task_list)
224
+
225
+ factsheet = areas.add_parser("factsheet", help="Create, list, and import fact sheets").add_subparsers(dest="action", required=True)
226
+ make_fs = factsheet.add_parser("create", help="Create from one JSON object")
227
+ make_fs.add_argument("file", help="Fact sheet JSON file")
228
+ make_fs.set_defaults(func=factsheet_create)
229
+ import_fs = factsheet.add_parser("import", help="Import a JSON file or ARCH-Insight ZIP export")
230
+ import_fs.add_argument("file")
231
+ import_fs.set_defaults(func=factsheet_import)
232
+ factsheet.add_parser("list").set_defaults(func=factsheet_list)
233
+
234
+ secrets_parser = areas.add_parser("secrets", help="Fetch, export, or push vault secrets")
235
+ secrets_parser.add_argument("secrets_args", nargs=argparse.REMAINDER)
236
+ secrets_parser.set_defaults(func=secrets_command)
237
+
238
+ repo = areas.add_parser("repo", help="Manage developer-platform repositories").add_subparsers(dest="action", required=True)
239
+ create_repo = repo.add_parser("create"); create_repo.add_argument("path"); create_repo.add_argument("--description", default=""); create_repo.set_defaults(func=repo_create)
240
+ repo.add_parser("list").set_defaults(func=repo_list)
241
+
242
+ pipeline = areas.add_parser("pipeline", help="Inspect and control CI pipelines").add_subparsers(dest="action", required=True)
243
+ lp = pipeline.add_parser("list"); lp.add_argument("--repo", type=int); lp.set_defaults(func=pipeline_list)
244
+ sp = pipeline.add_parser("show"); sp.add_argument("pipeline_id", type=int); sp.set_defaults(func=pipeline_show)
245
+ cp = pipeline.add_parser("cancel"); cp.add_argument("pipeline_id", type=int); cp.set_defaults(func=pipeline_cancel)
246
+ jl = pipeline.add_parser("logs"); jl.add_argument("job_id", type=int); jl.set_defaults(func=pipeline_logs)
247
+
248
+ registry = areas.add_parser("registry", help="Inspect the container registry").add_subparsers(dest="action", required=True)
249
+ registry.add_parser("list").set_defaults(func=registry_list)
250
+ rt = registry.add_parser("tags"); rt.add_argument("repository"); rt.set_defaults(func=registry_tags)
251
+
252
+ runner = areas.add_parser("runner", help="List registered CI runners").add_subparsers(dest="action", required=True)
253
+ runner.add_parser("list").set_defaults(func=runner_list)
254
+
255
+ deploy = areas.add_parser("deploy", help="List deployments").add_subparsers(dest="action", required=True)
256
+ dl = deploy.add_parser("list"); dl.add_argument("environment", nargs="?", default=""); dl.set_defaults(func=deploy_list)
257
+ return parser
258
+
259
+
260
+ def main(argv=None):
261
+ args = build_parser().parse_args(argv)
262
+ try:
263
+ result = args.func(args)
264
+ return result if isinstance(result, int) else 0
265
+ except CliError as exc:
266
+ print(f"arch: {exc}", file=sys.stderr)
267
+ return 2
268
+ except KeyboardInterrupt:
269
+ return 130
270
+
271
+
272
+ if __name__ == "__main__":
273
+ raise SystemExit(main())
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.4
2
+ Name: arch-insight-cli
3
+ Version: 0.1.0
4
+ Summary: Command-line client for ARCH-Insight tasks, factsheets, secrets, and developer platform.
5
+ Requires-Python: >=3.10
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: arch-secrets>=1.1.1
8
+
9
+ # arch-cli
10
+
11
+ `arch-cli` is the ARCH-Insight command-line client. It includes commands for
12
+ working with the product API, importing architecture data, managing the
13
+ developer platform, and using the secrets vault.
14
+
15
+ ## Install
16
+
17
+ ```sh
18
+ python -m pip install arch-insight-cli
19
+ ```
20
+
21
+ The installed command is `arch` (also available as `arch-cli`). Python 3.9 or
22
+ newer is required.
23
+
24
+ ## Authenticate
25
+
26
+ Product commands such as tasks and fact sheets require a GoTrue user access
27
+ token. Set it as `ARCH_ACCESS_TOKEN` or pass it with `--token`:
28
+
29
+ ```sh
30
+ export ARCH_ACCESS_TOKEN='your-user-access-token'
31
+ ```
32
+
33
+ Developer-platform commands also accept the platform token through
34
+ `ARCH_TOKEN`. Set `ARCH_API_URL` to use a self-hosted deployment; the default
35
+ is `https://arch-insight.com`.
36
+
37
+ ## Tasks
38
+
39
+ ```sh
40
+ arch task create "Review database costs" \
41
+ --description "Check the quarterly forecast" \
42
+ --project-id 12 --board-id 4 --priority high \
43
+ --assignee "Alex Example" --due-date 2026-12-01
44
+
45
+ arch task list --project-id 12
46
+ ```
47
+
48
+ The create command prints the created task as JSON. Repeat `--assignee` to
49
+ assign more than one person.
50
+
51
+ ## Fact sheets
52
+
53
+ Create one fact sheet from JSON, import one or more JSON objects, or import an
54
+ ARCH-Insight ZIP export:
55
+
56
+ ```sh
57
+ arch factsheet create application.json
58
+ arch factsheet import fact-sheets.json
59
+ arch factsheet import archi_insight_export.zip
60
+ arch factsheet list
61
+ ```
62
+
63
+ JSON imports create rows one at a time through the same API used by the web
64
+ app. If an item fails, earlier successful items remain created. ZIP imports use
65
+ the application's import endpoint and report imported and skipped entries.
66
+
67
+ ## Secrets
68
+
69
+ The dedicated `arch-secrets` command remains available. The same functionality
70
+ can be reached through `arch secrets`:
71
+
72
+ ```sh
73
+ export ARCH_SECRETS_TOKEN='your-vault-token'
74
+ arch secrets run -- npm start
75
+ arch secrets export > .env
76
+ arch secrets push .env
77
+ ```
78
+
79
+ The vault token is scoped independently from the user access token used by
80
+ product commands.
81
+
82
+ ## Developer platform
83
+
84
+ ```sh
85
+ arch repo list
86
+ arch repo create team/sample --description "Example repository"
87
+ arch pipeline list --repo 12
88
+ arch pipeline show 42
89
+ arch pipeline logs 99
90
+ arch registry list
91
+ arch registry tags team/sample
92
+ arch runner list
93
+ arch deploy list production
94
+ ```
@@ -0,0 +1,7 @@
1
+ arch_cli/__init__.py,sha256=nJROJiAYdnQudsRhZQDDEuBdFYDWghK6Pfr8Hk9Y8j8,63
2
+ arch_cli/main.py,sha256=hkBFsRpwjWFpdrrQvaRFCwlwLGhQMbAm6NNWchXyajM,11312
3
+ arch_insight_cli-0.1.0.dist-info/METADATA,sha256=D3wh3QodbX7mHUd4-U9aUQvHc7x92IqY2gf7d5tim8s,2618
4
+ arch_insight_cli-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
5
+ arch_insight_cli-0.1.0.dist-info/entry_points.txt,sha256=-Gu60u5RdNZEOkKOd6zIadBslpAd2N1zJKgAMB-ENjg,74
6
+ arch_insight_cli-0.1.0.dist-info/top_level.txt,sha256=B0NCScJnvAuzjddGyclArR5R_n9yV2hplUOufJImqSU,9
7
+ arch_insight_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ arch = arch_cli.main:main
3
+ arch-cli = arch_cli.main:main
@@ -0,0 +1 @@
1
+ arch_cli