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 +3 -0
- arch_cli/main.py +273 -0
- arch_insight_cli-0.1.0.dist-info/METADATA +94 -0
- arch_insight_cli-0.1.0.dist-info/RECORD +7 -0
- arch_insight_cli-0.1.0.dist-info/WHEEL +5 -0
- arch_insight_cli-0.1.0.dist-info/entry_points.txt +3 -0
- arch_insight_cli-0.1.0.dist-info/top_level.txt +1 -0
arch_cli/__init__.py
ADDED
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 @@
|
|
|
1
|
+
arch_cli
|