moss-cli 0.1.1__tar.gz → 0.2.1__tar.gz
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.
- {moss_cli-0.1.1/src/moss_cli.egg-info → moss_cli-0.2.1}/PKG-INFO +69 -24
- {moss_cli-0.1.1 → moss_cli-0.2.1}/README.md +58 -14
- {moss_cli-0.1.1 → moss_cli-0.2.1}/pyproject.toml +12 -11
- {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/__init__.py +1 -1
- moss_cli-0.2.1/src/moss_cli/api.py +106 -0
- moss_cli-0.2.1/src/moss_cli/commands/auth.py +249 -0
- moss_cli-0.2.1/src/moss_cli/commands/common.py +29 -0
- {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/commands/init_cmd.py +3 -2
- moss_cli-0.2.1/src/moss_cli/commands/keys.py +90 -0
- moss_cli-0.2.1/src/moss_cli/commands/projects.py +37 -0
- {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/commands/search.py +4 -13
- moss_cli-0.2.1/src/moss_cli/config.py +135 -0
- {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/main.py +19 -1
- {moss_cli-0.1.1 → moss_cli-0.2.1/src/moss_cli.egg-info}/PKG-INFO +69 -24
- {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli.egg-info/SOURCES.txt +7 -1
- moss_cli-0.2.1/src/moss_cli.egg-info/requires.txt +12 -0
- moss_cli-0.2.1/tests/test_login_and_keys.py +204 -0
- moss_cli-0.1.1/src/moss_cli/config.py +0 -57
- moss_cli-0.1.1/src/moss_cli.egg-info/requires.txt +0 -12
- {moss_cli-0.1.1 → moss_cli-0.2.1}/LICENSE +0 -0
- {moss_cli-0.1.1 → moss_cli-0.2.1}/setup.cfg +0 -0
- {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/commands/__init__.py +0 -0
- {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/commands/doc.py +0 -0
- {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/commands/index.py +0 -0
- {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/commands/job.py +0 -0
- {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/commands/version.py +0 -0
- {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/documents.py +0 -0
- {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/job_waiter.py +0 -0
- {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli/output.py +0 -0
- {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli.egg-info/dependency_links.txt +0 -0
- {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli.egg-info/entry_points.txt +0 -0
- {moss_cli-0.1.1 → moss_cli-0.2.1}/src/moss_cli.egg-info/top_level.txt +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: moss-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Command-line interface for Moss semantic search
|
|
5
5
|
Author-email: "InferEdge Inc." <contact@usemoss.dev>
|
|
6
|
+
License-Expression: LicenseRef-PolyForm-Shield-1.0.0
|
|
6
7
|
Keywords: search,semantic,cli,moss
|
|
7
8
|
Classifier: Development Status :: 3 - Alpha
|
|
8
9
|
Classifier: Intended Audience :: Developers
|
|
@@ -16,23 +17,23 @@ Requires-Python: >=3.10
|
|
|
16
17
|
Description-Content-Type: text/markdown
|
|
17
18
|
License-File: LICENSE
|
|
18
19
|
Requires-Dist: moss>=1.0.0
|
|
19
|
-
Requires-Dist: typer>=0.
|
|
20
|
-
Requires-Dist: rich>=
|
|
20
|
+
Requires-Dist: typer>=0.26.8
|
|
21
|
+
Requires-Dist: rich>=15.0.0
|
|
21
22
|
Provides-Extra: dev
|
|
22
|
-
Requires-Dist: pytest>=
|
|
23
|
-
Requires-Dist: black
|
|
24
|
-
Requires-Dist: isort
|
|
25
|
-
Requires-Dist: flake8>=7.
|
|
26
|
-
Requires-Dist: mypy>=1.0
|
|
27
|
-
Requires-Dist: build>=1.
|
|
28
|
-
Requires-Dist: twine>=
|
|
23
|
+
Requires-Dist: pytest>=9.1.1; extra == "dev"
|
|
24
|
+
Requires-Dist: black<26,>=25.9.0; extra == "dev"
|
|
25
|
+
Requires-Dist: isort<8,>=7.0.0; extra == "dev"
|
|
26
|
+
Requires-Dist: flake8>=7.3.0; extra == "dev"
|
|
27
|
+
Requires-Dist: mypy>=2.1.0; extra == "dev"
|
|
28
|
+
Requires-Dist: build>=1.5.0; extra == "dev"
|
|
29
|
+
Requires-Dist: twine>=6.2.0; extra == "dev"
|
|
29
30
|
Dynamic: license-file
|
|
30
31
|
|
|
31
32
|
# Moss CLI
|
|
32
33
|
|
|
33
34
|
**Command-line interface for Moss semantic search** — manage indexes, documents, and queries directly from your terminal.
|
|
34
35
|
|
|
35
|
-
Moss CLI wraps the [Moss Python SDK](https://docs.
|
|
36
|
+
Moss CLI wraps the [Moss Python SDK](https://docs.moss.dev/) so you can build and query semantic search indexes without writing any code. Ideal for quick prototyping, scripting, CI/CD pipelines, and data workflows.
|
|
36
37
|
|
|
37
38
|
## Features
|
|
38
39
|
|
|
@@ -40,8 +41,9 @@ Moss CLI wraps the [Moss Python SDK](https://docs.usemoss.dev/) so you can build
|
|
|
40
41
|
- **Semantic search** — query indexes with configurable hybrid search (semantic + keyword)
|
|
41
42
|
- **Index management** — create, list, inspect, and delete indexes
|
|
42
43
|
- **Document management** — add, update, retrieve, and delete documents
|
|
43
|
-
- **Local by default** — downloads indexes
|
|
44
|
+
- **Local by default** — downloads indexes and queries them on-device
|
|
44
45
|
- **Flexible auth** — CLI flags, environment variables, or config file
|
|
46
|
+
- **Log in from the terminal** — `moss login`, then create project keys with `moss keys create`
|
|
45
47
|
- **Multiple output formats** — rich tables for humans, `--json` for scripts
|
|
46
48
|
- **Job tracking** — poll background jobs with live progress display
|
|
47
49
|
- **Pipe-friendly** — stdin/stdout support for composing with other tools
|
|
@@ -66,9 +68,6 @@ moss index create my-index -f docs.json --wait
|
|
|
66
68
|
|
|
67
69
|
# 4. Search it
|
|
68
70
|
moss query my-index "what is machine learning"
|
|
69
|
-
|
|
70
|
-
# 5. Search via cloud API (skips local download)
|
|
71
|
-
moss query my-index "neural networks" --cloud
|
|
72
71
|
```
|
|
73
72
|
|
|
74
73
|
## Authentication
|
|
@@ -88,9 +87,56 @@ export MOSS_PROJECT_ID="your-project-id"
|
|
|
88
87
|
export MOSS_PROJECT_KEY="your-project-key"
|
|
89
88
|
|
|
90
89
|
# Option 3: Inline flags
|
|
91
|
-
moss
|
|
90
|
+
moss --project-id "..." --project-key "..." index list
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
## Log in and create keys
|
|
94
|
+
|
|
95
|
+
`moss login` connects the CLI to your Moss account, so you can create project keys without opening the dashboard.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
# Opens the browser. Check the code matches, then click Approve.
|
|
99
|
+
moss login
|
|
100
|
+
|
|
101
|
+
moss whoami
|
|
102
|
+
moss projects list
|
|
103
|
+
|
|
104
|
+
# Prints the key once, plus MOSS_PROJECT_ID / MOSS_PROJECT_KEY lines
|
|
105
|
+
moss keys create --project <project-id> --name "prod server"
|
|
106
|
+
moss keys list --project <project-id>
|
|
107
|
+
moss keys delete --project <project-id> <key-id>
|
|
108
|
+
|
|
109
|
+
# Revoke the token and remove it from ~/.moss/config.json
|
|
110
|
+
moss logout
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The token is saved to `~/.moss/config.json`. It lives until you run `moss logout` or revoke it in Settings > API Keys. It acts as you: only workspace owners, admins and a project's owner can create or delete its keys. `--project` defaults to the project saved by `moss init`.
|
|
114
|
+
|
|
115
|
+
Use `moss login --no-browser` on a machine without a browser (for example over SSH) and open the printed link anywhere.
|
|
116
|
+
|
|
117
|
+
## Agents and CI
|
|
118
|
+
|
|
119
|
+
CI that only queries or uploads to one project needs a project key, not a login. Create one with `moss keys create` and store it as `MOSS_PROJECT_ID` / `MOSS_PROJECT_KEY`.
|
|
120
|
+
|
|
121
|
+
Agents that create or delete keys need a CLI token. Make a separate one for each agent, so logging in or out on your machine doesn't break it:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
# Approve in the browser; the token is printed, not saved
|
|
125
|
+
moss login --print-token --name "deploy agent"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Store it as `MOSS_TOKEN` in your secret store, then:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
export MOSS_TOKEN="mossc_..."
|
|
132
|
+
moss keys create --project <project-id> --json
|
|
133
|
+
# {"project_id": "...", "key_id": "...", "secret": "moss_..."}
|
|
92
134
|
```
|
|
93
135
|
|
|
136
|
+
The token acts as you, so it loses access to a workspace when you do. Revoke it in Settings > API Keys.
|
|
137
|
+
|
|
138
|
+
The token is resolved from `--token`, then `MOSS_TOKEN`, then the config file. The saved token is only sent to the dashboard that issued it, so with `--api-url` pointing elsewhere, log in there or pass `--token`. A `--token` or `MOSS_TOKEN` goes to production unless `--api-url` / `MOSS_API_URL` says otherwise.
|
|
139
|
+
|
|
94
140
|
## Commands
|
|
95
141
|
|
|
96
142
|
### Index Management
|
|
@@ -141,9 +187,6 @@ moss query my-index "what is deep learning"
|
|
|
141
187
|
# Tune results: more results, keyword-heavy
|
|
142
188
|
moss query my-index "neural networks" --top-k 20 --alpha 0.3
|
|
143
189
|
|
|
144
|
-
# Cloud mode (skip download, query via cloud API)
|
|
145
|
-
moss query my-index "transformers" --cloud
|
|
146
|
-
|
|
147
190
|
# With metadata filter (local only)
|
|
148
191
|
moss query my-index "shoes" --filter '{"field": "category", "condition": {"$eq": "footwear"}}'
|
|
149
192
|
|
|
@@ -151,7 +194,7 @@ moss query my-index "shoes" --filter '{"field": "category", "condition": {"$eq":
|
|
|
151
194
|
echo "what is AI" | moss query my-index
|
|
152
195
|
|
|
153
196
|
# JSON output for scripting
|
|
154
|
-
moss query my-index "query"
|
|
197
|
+
moss --json query my-index "query" | jq '.docs[0].text'
|
|
155
198
|
```
|
|
156
199
|
|
|
157
200
|
### Job Tracking
|
|
@@ -170,9 +213,9 @@ moss job status <job-id> --wait
|
|
|
170
213
|
# Print version info
|
|
171
214
|
moss version
|
|
172
215
|
|
|
173
|
-
# Global JSON output
|
|
174
|
-
moss index list
|
|
175
|
-
moss doc get my-index
|
|
216
|
+
# Global JSON output, before the command
|
|
217
|
+
moss --json index list
|
|
218
|
+
moss --json doc get my-index
|
|
176
219
|
```
|
|
177
220
|
|
|
178
221
|
## Document File Format
|
|
@@ -211,6 +254,8 @@ cat docs.json | moss doc add my-index -f -
|
|
|
211
254
|
|---|---|---|
|
|
212
255
|
| `--project-id` | `-p` | Project ID (overrides env/config) |
|
|
213
256
|
| `--project-key` | | Project key (overrides env/config) |
|
|
257
|
+
| `--token` | | CLI token from `moss login` (or `MOSS_TOKEN`) |
|
|
258
|
+
| `--api-url` | | Dashboard URL, for staging or local testing (or `MOSS_API_URL`) |
|
|
214
259
|
| `--json` | | Machine-readable JSON output |
|
|
215
260
|
| `--verbose` | `-v` | Enable debug logging |
|
|
216
261
|
|
|
@@ -226,4 +271,4 @@ cat docs.json | moss doc add my-index -f -
|
|
|
226
271
|
|
|
227
272
|
Copyright (c) 2026 InferEdge Inc. — PolyForm Shield License 1.0.0.
|
|
228
273
|
|
|
229
|
-
See [LICENSE](LICENSE) for full terms. For commercial licensing, contact contact@
|
|
274
|
+
See [LICENSE](LICENSE) for full terms. For commercial licensing, contact contact@moss.dev.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Command-line interface for Moss semantic search** — manage indexes, documents, and queries directly from your terminal.
|
|
4
4
|
|
|
5
|
-
Moss CLI wraps the [Moss Python SDK](https://docs.
|
|
5
|
+
Moss CLI wraps the [Moss Python SDK](https://docs.moss.dev/) so you can build and query semantic search indexes without writing any code. Ideal for quick prototyping, scripting, CI/CD pipelines, and data workflows.
|
|
6
6
|
|
|
7
7
|
## Features
|
|
8
8
|
|
|
@@ -10,8 +10,9 @@ Moss CLI wraps the [Moss Python SDK](https://docs.usemoss.dev/) so you can build
|
|
|
10
10
|
- **Semantic search** — query indexes with configurable hybrid search (semantic + keyword)
|
|
11
11
|
- **Index management** — create, list, inspect, and delete indexes
|
|
12
12
|
- **Document management** — add, update, retrieve, and delete documents
|
|
13
|
-
- **Local by default** — downloads indexes
|
|
13
|
+
- **Local by default** — downloads indexes and queries them on-device
|
|
14
14
|
- **Flexible auth** — CLI flags, environment variables, or config file
|
|
15
|
+
- **Log in from the terminal** — `moss login`, then create project keys with `moss keys create`
|
|
15
16
|
- **Multiple output formats** — rich tables for humans, `--json` for scripts
|
|
16
17
|
- **Job tracking** — poll background jobs with live progress display
|
|
17
18
|
- **Pipe-friendly** — stdin/stdout support for composing with other tools
|
|
@@ -36,9 +37,6 @@ moss index create my-index -f docs.json --wait
|
|
|
36
37
|
|
|
37
38
|
# 4. Search it
|
|
38
39
|
moss query my-index "what is machine learning"
|
|
39
|
-
|
|
40
|
-
# 5. Search via cloud API (skips local download)
|
|
41
|
-
moss query my-index "neural networks" --cloud
|
|
42
40
|
```
|
|
43
41
|
|
|
44
42
|
## Authentication
|
|
@@ -58,9 +56,56 @@ export MOSS_PROJECT_ID="your-project-id"
|
|
|
58
56
|
export MOSS_PROJECT_KEY="your-project-key"
|
|
59
57
|
|
|
60
58
|
# Option 3: Inline flags
|
|
61
|
-
moss
|
|
59
|
+
moss --project-id "..." --project-key "..." index list
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Log in and create keys
|
|
63
|
+
|
|
64
|
+
`moss login` connects the CLI to your Moss account, so you can create project keys without opening the dashboard.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# Opens the browser. Check the code matches, then click Approve.
|
|
68
|
+
moss login
|
|
69
|
+
|
|
70
|
+
moss whoami
|
|
71
|
+
moss projects list
|
|
72
|
+
|
|
73
|
+
# Prints the key once, plus MOSS_PROJECT_ID / MOSS_PROJECT_KEY lines
|
|
74
|
+
moss keys create --project <project-id> --name "prod server"
|
|
75
|
+
moss keys list --project <project-id>
|
|
76
|
+
moss keys delete --project <project-id> <key-id>
|
|
77
|
+
|
|
78
|
+
# Revoke the token and remove it from ~/.moss/config.json
|
|
79
|
+
moss logout
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The token is saved to `~/.moss/config.json`. It lives until you run `moss logout` or revoke it in Settings > API Keys. It acts as you: only workspace owners, admins and a project's owner can create or delete its keys. `--project` defaults to the project saved by `moss init`.
|
|
83
|
+
|
|
84
|
+
Use `moss login --no-browser` on a machine without a browser (for example over SSH) and open the printed link anywhere.
|
|
85
|
+
|
|
86
|
+
## Agents and CI
|
|
87
|
+
|
|
88
|
+
CI that only queries or uploads to one project needs a project key, not a login. Create one with `moss keys create` and store it as `MOSS_PROJECT_ID` / `MOSS_PROJECT_KEY`.
|
|
89
|
+
|
|
90
|
+
Agents that create or delete keys need a CLI token. Make a separate one for each agent, so logging in or out on your machine doesn't break it:
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Approve in the browser; the token is printed, not saved
|
|
94
|
+
moss login --print-token --name "deploy agent"
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Store it as `MOSS_TOKEN` in your secret store, then:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
export MOSS_TOKEN="mossc_..."
|
|
101
|
+
moss keys create --project <project-id> --json
|
|
102
|
+
# {"project_id": "...", "key_id": "...", "secret": "moss_..."}
|
|
62
103
|
```
|
|
63
104
|
|
|
105
|
+
The token acts as you, so it loses access to a workspace when you do. Revoke it in Settings > API Keys.
|
|
106
|
+
|
|
107
|
+
The token is resolved from `--token`, then `MOSS_TOKEN`, then the config file. The saved token is only sent to the dashboard that issued it, so with `--api-url` pointing elsewhere, log in there or pass `--token`. A `--token` or `MOSS_TOKEN` goes to production unless `--api-url` / `MOSS_API_URL` says otherwise.
|
|
108
|
+
|
|
64
109
|
## Commands
|
|
65
110
|
|
|
66
111
|
### Index Management
|
|
@@ -111,9 +156,6 @@ moss query my-index "what is deep learning"
|
|
|
111
156
|
# Tune results: more results, keyword-heavy
|
|
112
157
|
moss query my-index "neural networks" --top-k 20 --alpha 0.3
|
|
113
158
|
|
|
114
|
-
# Cloud mode (skip download, query via cloud API)
|
|
115
|
-
moss query my-index "transformers" --cloud
|
|
116
|
-
|
|
117
159
|
# With metadata filter (local only)
|
|
118
160
|
moss query my-index "shoes" --filter '{"field": "category", "condition": {"$eq": "footwear"}}'
|
|
119
161
|
|
|
@@ -121,7 +163,7 @@ moss query my-index "shoes" --filter '{"field": "category", "condition": {"$eq":
|
|
|
121
163
|
echo "what is AI" | moss query my-index
|
|
122
164
|
|
|
123
165
|
# JSON output for scripting
|
|
124
|
-
moss query my-index "query"
|
|
166
|
+
moss --json query my-index "query" | jq '.docs[0].text'
|
|
125
167
|
```
|
|
126
168
|
|
|
127
169
|
### Job Tracking
|
|
@@ -140,9 +182,9 @@ moss job status <job-id> --wait
|
|
|
140
182
|
# Print version info
|
|
141
183
|
moss version
|
|
142
184
|
|
|
143
|
-
# Global JSON output
|
|
144
|
-
moss index list
|
|
145
|
-
moss doc get my-index
|
|
185
|
+
# Global JSON output, before the command
|
|
186
|
+
moss --json index list
|
|
187
|
+
moss --json doc get my-index
|
|
146
188
|
```
|
|
147
189
|
|
|
148
190
|
## Document File Format
|
|
@@ -181,6 +223,8 @@ cat docs.json | moss doc add my-index -f -
|
|
|
181
223
|
|---|---|---|
|
|
182
224
|
| `--project-id` | `-p` | Project ID (overrides env/config) |
|
|
183
225
|
| `--project-key` | | Project key (overrides env/config) |
|
|
226
|
+
| `--token` | | CLI token from `moss login` (or `MOSS_TOKEN`) |
|
|
227
|
+
| `--api-url` | | Dashboard URL, for staging or local testing (or `MOSS_API_URL`) |
|
|
184
228
|
| `--json` | | Machine-readable JSON output |
|
|
185
229
|
| `--verbose` | `-v` | Enable debug logging |
|
|
186
230
|
|
|
@@ -196,4 +240,4 @@ cat docs.json | moss doc add my-index -f -
|
|
|
196
240
|
|
|
197
241
|
Copyright (c) 2026 InferEdge Inc. — PolyForm Shield License 1.0.0.
|
|
198
242
|
|
|
199
|
-
See [LICENSE](LICENSE) for full terms. For commercial licensing, contact contact@
|
|
243
|
+
See [LICENSE](LICENSE) for full terms. For commercial licensing, contact contact@moss.dev.
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = ["setuptools>=
|
|
2
|
+
requires = ["setuptools>=77.0", "wheel"]
|
|
3
3
|
build-backend = "setuptools.build_meta"
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "moss-cli"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.2.1"
|
|
8
8
|
description = "Command-line interface for Moss semantic search"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
|
+
license = "LicenseRef-PolyForm-Shield-1.0.0"
|
|
11
12
|
license-files = ["LICENSE"]
|
|
12
13
|
authors = [
|
|
13
14
|
{ name = "InferEdge Inc.", email = "contact@usemoss.dev" }
|
|
@@ -25,19 +26,19 @@ classifiers = [
|
|
|
25
26
|
]
|
|
26
27
|
dependencies = [
|
|
27
28
|
"moss>=1.0.0",
|
|
28
|
-
"typer>=0.
|
|
29
|
-
"rich>=
|
|
29
|
+
"typer>=0.26.8",
|
|
30
|
+
"rich>=15.0.0",
|
|
30
31
|
]
|
|
31
32
|
|
|
32
33
|
[project.optional-dependencies]
|
|
33
34
|
dev = [
|
|
34
|
-
"pytest>=
|
|
35
|
-
"black>=
|
|
36
|
-
"isort>=
|
|
37
|
-
"flake8>=7.
|
|
38
|
-
"mypy>=1.0
|
|
39
|
-
"build>=1.
|
|
40
|
-
"twine>=
|
|
35
|
+
"pytest>=9.1.1",
|
|
36
|
+
"black>=25.9.0,<26",
|
|
37
|
+
"isort>=7.0.0,<8",
|
|
38
|
+
"flake8>=7.3.0",
|
|
39
|
+
"mypy>=2.1.0",
|
|
40
|
+
"build>=1.5.0",
|
|
41
|
+
"twine>=6.2.0",
|
|
41
42
|
]
|
|
42
43
|
|
|
43
44
|
[project.scripts]
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""HTTP client for the dashboard's CLI routes (login, projects, keys)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import urllib.error
|
|
7
|
+
import urllib.request
|
|
8
|
+
from typing import Any, Dict, Optional, Tuple
|
|
9
|
+
|
|
10
|
+
import moss_cli
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ApiError(Exception):
|
|
14
|
+
def __init__(self, status: int, message: str, code: Optional[str] = None) -> None:
|
|
15
|
+
super().__init__(message)
|
|
16
|
+
self.status = status
|
|
17
|
+
self.code = code
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
21
|
+
# urllib copies the Authorization header onto a redirect, even to another host.
|
|
22
|
+
def redirect_request(self, *args: Any, **kwargs: Any) -> None:
|
|
23
|
+
return None
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_opener = urllib.request.build_opener(_NoRedirect)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def request(
|
|
30
|
+
api_url: str,
|
|
31
|
+
method: str,
|
|
32
|
+
path: str,
|
|
33
|
+
token: Optional[str] = None,
|
|
34
|
+
body: Optional[Dict[str, Any]] = None,
|
|
35
|
+
) -> Tuple[int, Dict[str, Any]]:
|
|
36
|
+
"""Send a request and return (status, json body). Never raises on an HTTP error status.
|
|
37
|
+
|
|
38
|
+
Raises ApiError only when the server doesn't answer like a Moss dashboard with CLI
|
|
39
|
+
login: a redirect or an HTML page. That's the sign of an old dashboard or a wrong
|
|
40
|
+
--api-url, not a transient failure, so a retry loop must not swallow it.
|
|
41
|
+
"""
|
|
42
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
43
|
+
headers = {
|
|
44
|
+
"Accept": "application/json",
|
|
45
|
+
"User-Agent": f"moss-cli/{moss_cli.__version__}",
|
|
46
|
+
}
|
|
47
|
+
if data is not None:
|
|
48
|
+
headers["Content-Type"] = "application/json"
|
|
49
|
+
if token:
|
|
50
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
51
|
+
req = urllib.request.Request(
|
|
52
|
+
api_url.rstrip("/") + path, data=data, headers=headers, method=method
|
|
53
|
+
)
|
|
54
|
+
try:
|
|
55
|
+
with _opener.open(req, timeout=30) as res:
|
|
56
|
+
status, content_type, raw = (
|
|
57
|
+
res.status,
|
|
58
|
+
res.headers.get("Content-Type"),
|
|
59
|
+
res.read(),
|
|
60
|
+
)
|
|
61
|
+
except urllib.error.HTTPError as e:
|
|
62
|
+
status, content_type, raw = e.code, e.headers.get("Content-Type"), e.read()
|
|
63
|
+
|
|
64
|
+
# An old dashboard answers these routes with a redirect to the login page, and a wrong
|
|
65
|
+
# --api-url may answer with HTML. A 5xx or 429 may be a gateway page, which callers
|
|
66
|
+
# retry, so it stays a plain status.
|
|
67
|
+
retryable = status >= 500 or status == 429
|
|
68
|
+
if 300 <= status < 400 or (not retryable and raw and not _is_json(content_type)):
|
|
69
|
+
raise ApiError(
|
|
70
|
+
status,
|
|
71
|
+
f"{api_url} did not answer like a Moss dashboard with CLI login (HTTP {status}). "
|
|
72
|
+
"Check --api-url / MOSS_API_URL, or ask for the dashboard to be updated.",
|
|
73
|
+
)
|
|
74
|
+
return status, _parse(raw)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def call(
|
|
78
|
+
api_url: str,
|
|
79
|
+
method: str,
|
|
80
|
+
path: str,
|
|
81
|
+
token: Optional[str] = None,
|
|
82
|
+
body: Optional[Dict[str, Any]] = None,
|
|
83
|
+
) -> Dict[str, Any]:
|
|
84
|
+
"""Like request(), but raises ApiError for any non-2xx status."""
|
|
85
|
+
status, payload = request(api_url, method, path, token, body)
|
|
86
|
+
if status >= 400:
|
|
87
|
+
message = payload.get("error") or f"Request failed ({status})"
|
|
88
|
+
if status == 401 and token:
|
|
89
|
+
message = (
|
|
90
|
+
"Your CLI token is invalid or revoked. Run 'moss login' again, or replace "
|
|
91
|
+
"MOSS_TOKEN / --token with a new one from 'moss login --print-token'."
|
|
92
|
+
)
|
|
93
|
+
raise ApiError(status, message, payload.get("code"))
|
|
94
|
+
return payload
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _is_json(content_type: Optional[str]) -> bool:
|
|
98
|
+
return "json" in (content_type or "").lower()
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _parse(raw: bytes) -> Dict[str, Any]:
|
|
102
|
+
try:
|
|
103
|
+
parsed = json.loads(raw or b"{}")
|
|
104
|
+
except json.JSONDecodeError:
|
|
105
|
+
return {}
|
|
106
|
+
return parsed if isinstance(parsed, dict) else {}
|