parse-sdk 0.1.0__tar.gz → 0.2.0__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.
- parse_sdk-0.2.0/CHANGELOG.md +32 -0
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/PKG-INFO +65 -15
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/README.md +64 -14
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/parse_sdk/__init__.py +2 -2
- parse_sdk-0.2.0/parse_sdk/_identifiers.py +23 -0
- parse_sdk-0.2.0/parse_sdk/_project.py +277 -0
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/parse_sdk/_sync.py +131 -53
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/parse_sdk/cli.py +779 -279
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/parse_sdk/cli_help.py +43 -22
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/parse_sdk/config.py +6 -5
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/parse_sdk/doctor.py +215 -41
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/parse_sdk/preview.py +48 -1
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/parse_sdk/sample_gate.py +13 -3
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/parse_sdk/scaffold.py +4 -3
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/pyproject.toml +1 -1
- parse_sdk-0.1.0/CHANGELOG.md +0 -18
- parse_sdk-0.1.0/parse_sdk/_project.py +0 -184
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/.gitignore +0 -0
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/LICENSE +0 -0
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/parse_sdk/_labels.py +0 -0
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/parse_sdk/_oauth.py +0 -0
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/parse_sdk/_runtime.py +0 -0
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/parse_sdk/_sanitize.py +0 -0
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/parse_sdk/checks.py +0 -0
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/parse_sdk/codegen_reconcile.py +0 -0
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/parse_sdk/codegen_v2.py +0 -0
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/parse_sdk/docgen.py +0 -0
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/parse_sdk/migrate.py +0 -0
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/parse_sdk/py.typed +0 -0
- {parse_sdk-0.1.0 → parse_sdk-0.2.0}/parse_sdk/resource_surface.py +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `parse-sdk` are documented here. This project follows
|
|
4
|
+
[Semantic Versioning](https://semver.org/).
|
|
5
|
+
|
|
6
|
+
## [0.2.0] — reliable sync and improved CLI workflows
|
|
7
|
+
|
|
8
|
+
- **Reliable reconciliation:** stronger validation and staged promotion make sync
|
|
9
|
+
more predictable when schemas or project configuration change.
|
|
10
|
+
- **Consistent API identity:** account and marketplace workflows now handle API
|
|
11
|
+
selection consistently across `init`, `add`, `remove`, and sync.
|
|
12
|
+
- **Improved CLI automation:** commands provide clearer guidance, predictable
|
|
13
|
+
JSON output and exit behavior, API selectors for `init`, and more focused sync
|
|
14
|
+
results.
|
|
15
|
+
- **Project diagnostics:** `parse doctor` performs read-only inspection with
|
|
16
|
+
fewer assumptions about the local project environment.
|
|
17
|
+
- **Documentation:** refreshed the command reference, SDK-versus-MCP guidance,
|
|
18
|
+
credential setup, and development workflow.
|
|
19
|
+
|
|
20
|
+
## [0.1.0] — first public release
|
|
21
|
+
|
|
22
|
+
Initial public release of the `parse-sdk` runtime + `parse` CLI: a
|
|
23
|
+
dynamic-yet-typed Python SDK that generates a project-local, fully typed client
|
|
24
|
+
for the Parse APIs your key can call.
|
|
25
|
+
|
|
26
|
+
- **CLI** (`parse`): `init`, `sync`, `login` (API key or browser OAuth via
|
|
27
|
+
`--web`), `doctor` (diagnose / `--fix`), `list`, `whoami`, `help`, `clean`,
|
|
28
|
+
and marketplace `add` / `search` / `remove`, plus `[tool.parse]`-driven
|
|
29
|
+
selective sync.
|
|
30
|
+
- **Typed runtime**: resources, collections, pagination, retries with a
|
|
31
|
+
wall-clock budget, and host-bound authentication.
|
|
32
|
+
- Ships `py.typed`; supports Python 3.10–3.14.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: parse-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: Dynamic-yet-typed Python SDK for Parse APIs
|
|
5
5
|
Project-URL: Homepage, https://parse.bot
|
|
6
6
|
Project-URL: Changelog, https://pypi.org/project/parse-sdk/#history
|
|
@@ -32,11 +32,21 @@ Description-Content-Type: text/markdown
|
|
|
32
32
|
|
|
33
33
|
Typed Python SDK tooling for [Parse](https://parse.bot) APIs.
|
|
34
34
|
|
|
35
|
+
Requires Python 3.10 or newer.
|
|
36
|
+
|
|
35
37
|
`parse-sdk` is the shared **runtime + CLI**. It generates a project-local
|
|
36
38
|
`parse_apis` package — a real, installed, editable dependency — that holds the
|
|
37
39
|
typed client for every API your key can call. Your application code imports from
|
|
38
40
|
`parse_apis`; `parse-sdk` provides the runtime bases and errors those clients use.
|
|
39
41
|
|
|
42
|
+
## SDK and MCP
|
|
43
|
+
|
|
44
|
+
Use Parse MCP when an agent is discovering or calling APIs ad hoc, or managing
|
|
45
|
+
account-owned server-side variants. Use this SDK for durable, typed Python
|
|
46
|
+
clients committed to an application. Neither requires the other: the SDK can
|
|
47
|
+
use `parse search` and `parse add --marketplace <listing_id>` without creating
|
|
48
|
+
an MCP subscription.
|
|
49
|
+
|
|
40
50
|
## Quickstart
|
|
41
51
|
|
|
42
52
|
In a uv-managed project (`uv init` first if you don't have a `pyproject.toml`):
|
|
@@ -46,9 +56,10 @@ uv add parse-sdk # installs the runtime + `parse` CLI into your proje
|
|
|
46
56
|
uv run parse init # prompts for your API key, then scaffolds + syncs parse_apis
|
|
47
57
|
```
|
|
48
58
|
|
|
49
|
-
Non-interactive (CI, agents, scripts):
|
|
50
|
-
`uv run parse login --api-key
|
|
51
|
-
|
|
59
|
+
Non-interactive (CI, agents, scripts): export `PARSE_API_KEY`, then run
|
|
60
|
+
`uv run parse init`. `parse login --api-key` persists a key, but is a less-safe
|
|
61
|
+
automation fallback because argv can be recorded in process listings and shell
|
|
62
|
+
history.
|
|
52
63
|
|
|
53
64
|
`parse init` is the one-time setup. It:
|
|
54
65
|
|
|
@@ -74,8 +85,9 @@ except RateLimitError:
|
|
|
74
85
|
...
|
|
75
86
|
```
|
|
76
87
|
|
|
77
|
-
(The exact methods are generated from *your* API's spec —
|
|
78
|
-
and `example.py` document each
|
|
88
|
+
(The exact methods are generated from *your* API's spec —
|
|
89
|
+
`parse_apis/src/parse_apis/<slug>/README.md` and `example.py` document each
|
|
90
|
+
client's real surface.)
|
|
79
91
|
|
|
80
92
|
```bash
|
|
81
93
|
uv run python app.py # run your app in the project environment
|
|
@@ -99,16 +111,17 @@ they just `import parse_apis`.
|
|
|
99
111
|
|
|
100
112
|
```text
|
|
101
113
|
parse_apis/
|
|
114
|
+
AGENTS.md CLAUDE.md # committed, byte-identical agent pointers
|
|
102
115
|
pyproject.toml # committed: name="parse-apis", pinned parse-sdk==X
|
|
103
116
|
src/parse_apis/
|
|
104
117
|
__init__.py # committed scaffold — re-exports runtime errors
|
|
105
118
|
py.typed # committed
|
|
106
|
-
|
|
119
|
+
reddit_com_api/__init__.py # generated (gitignored)
|
|
107
120
|
_manifest.json # generated (gitignored)
|
|
108
121
|
AGENTS.md CLAUDE.md # generated cross-API index (gitignored)
|
|
109
122
|
```
|
|
110
123
|
|
|
111
|
-
Commit the
|
|
124
|
+
Commit the five scaffold files; the generated payload is per-key (it reveals your API
|
|
112
125
|
inventory) and stays gitignored. `parse sync` builds the next payload in a staging tree,
|
|
113
126
|
self-tests it (secret scan, import-model, no-stale-docs), and promotes it with an atomic
|
|
114
127
|
directory swap — a failed sync never leaves broken or secret-bearing files in your project.
|
|
@@ -138,14 +151,51 @@ succeeds on a clean checkout.
|
|
|
138
151
|
|
|
139
152
|
| Command | What it does |
|
|
140
153
|
| --- | --- |
|
|
141
|
-
| `parse login` | Save
|
|
142
|
-
| `parse init` | One-time: ensure creds, scaffold `parse_apis`, record deps + gitignore, first sync. |
|
|
143
|
-
| `parse sync` | Regenerate the typed payload into `parse_apis/src/parse_apis` (staged + atomic). |
|
|
144
|
-
| `parse clean` | Remove generated payload; keep the committed scaffold. |
|
|
154
|
+
| `parse login` | Save browser OAuth credentials by default, or an API key with `--api-key`. No project changes. |
|
|
155
|
+
| `parse init` | One-time: ensure creds, scaffold `parse_apis`, record deps + gitignore, first sync. Pass slugs to track just those APIs. |
|
|
145
156
|
| `parse list` | Print the APIs the current key can call (no generation). |
|
|
157
|
+
| `parse sync` | Regenerate the typed payload into `parse_apis/src/parse_apis` (staged + atomic). `--check` is a CI dry run. |
|
|
158
|
+
| `parse add` | Add APIs to the repo's synced set (`--marketplace` for shared canonicals; new entries pin the latest usable release, while re-add preserves existing pin/live state), then reconcile. |
|
|
159
|
+
| `parse remove` | Stop syncing APIs (account or marketplace) and prune their generated clients. |
|
|
160
|
+
| `parse search` | Search the Parse marketplace for ready-made APIs. |
|
|
161
|
+
| `parse clean` | Remove generated payload; keep the committed scaffold. |
|
|
146
162
|
| `parse whoami` | Show the current key + base URL. |
|
|
163
|
+
| `parse doctor` | Diagnose the project's SDK install; `--fix` applies safe remediations. |
|
|
164
|
+
| `parse help` | Guided getting-started workflow (`--json` for machine-readable orientation). |
|
|
147
165
|
|
|
148
|
-
Run `parse <cmd> --help` for full per-command help
|
|
166
|
+
Run `parse <cmd> --help` for full per-command help — every command's help ends
|
|
167
|
+
with copy-pasteable Examples.
|
|
168
|
+
|
|
169
|
+
## Agents & automation
|
|
170
|
+
|
|
171
|
+
The CLI is built to run headless: prompts only fire on an interactive terminal
|
|
172
|
+
(non-TTY runs fail fast with the exact fix), warnings go to stderr, and stdout
|
|
173
|
+
stays machine-parseable under `--json`.
|
|
174
|
+
|
|
175
|
+
**Machine-readable output.** `parse help --json` is the orientation entrypoint:
|
|
176
|
+
it lists every command with `requires_auth` / `requires_init` facts and (under
|
|
177
|
+
`agent_modes`) which commands accept `--json` — the list is derived from the
|
|
178
|
+
live command registry, so it never goes stale. The `--json` shapes are stable:
|
|
179
|
+
|
|
180
|
+
| Command | Shape |
|
|
181
|
+
| --- | --- |
|
|
182
|
+
| `whoami --json` | object: `base_url`, `api_key_masked`, `key_configured`, `key_sent_to_host`, `web_login`, `auth_method` |
|
|
183
|
+
| `list --json` | array of `{id, slug, name, modeled, endpoints, resources, version}` |
|
|
184
|
+
| `sync` / `add` / `remove` `--json` | object: `written[] {slug, id, root, status, warnings[]}`, `skipped[] {slug, id, reason}`, `removed[]`, `quarantined[] {slug, id, retained, findings[]}`, `failures[]` — on a gate abort, `{failures}` alone with exit 1; under `sync --check`, `failures` carries every finding (non-empty ⟺ exit 1) |
|
|
185
|
+
| `doctor --json` | object: `ok`, `findings[] {level, title, detail, fix, kind}` |
|
|
186
|
+
| `search --json` | array of `{id, slug, name, source_url, endpoint_count, is_authenticated}` |
|
|
187
|
+
|
|
188
|
+
**Exit codes.** `0` — success. `1` — the command ran and detected a problem
|
|
189
|
+
(sync's pre-promotion gate abort, `sync --check` findings, `doctor` error-level
|
|
190
|
+
findings, `init` refusing to migrate a legacy tree with non-Parse files, a lost
|
|
191
|
+
concurrent-promote race). `2` — the command couldn't run: usage errors, an
|
|
192
|
+
uninitialized project, missing/withheld credentials or an untrusted host,
|
|
193
|
+
network failures.
|
|
194
|
+
|
|
195
|
+
**Credentials in automation.** Prefer exporting `PARSE_API_KEY` over passing
|
|
196
|
+
`--api-key` on the command line — argv is visible in process listings and shell
|
|
197
|
+
history. `parse login` (browser OAuth by default; `--web` is explicit) requires an interactive terminal
|
|
198
|
+
and fails fast headless.
|
|
149
199
|
|
|
150
200
|
## Versioning
|
|
151
201
|
|
|
@@ -157,8 +207,8 @@ regenerate against the new runtime; `init`/`sync` restamp the pin.
|
|
|
157
207
|
|
|
158
208
|
| Var | Default | Notes |
|
|
159
209
|
| --- | --- | --- |
|
|
160
|
-
| `PARSE_API_KEY` | – | Auth for real requests (or use `parse login`). |
|
|
161
|
-
| `
|
|
210
|
+
| `PARSE_API_KEY` | – | Auth for real requests (or use `parse login`). Preferred over `--api-key` in automation. |
|
|
211
|
+
| `PARSE_ACCESS_TOKEN` | – | OAuth bearer token override (normally managed by interactive `parse login`). |
|
|
162
212
|
|
|
163
213
|
## Changelog
|
|
164
214
|
|
|
@@ -2,11 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
Typed Python SDK tooling for [Parse](https://parse.bot) APIs.
|
|
4
4
|
|
|
5
|
+
Requires Python 3.10 or newer.
|
|
6
|
+
|
|
5
7
|
`parse-sdk` is the shared **runtime + CLI**. It generates a project-local
|
|
6
8
|
`parse_apis` package — a real, installed, editable dependency — that holds the
|
|
7
9
|
typed client for every API your key can call. Your application code imports from
|
|
8
10
|
`parse_apis`; `parse-sdk` provides the runtime bases and errors those clients use.
|
|
9
11
|
|
|
12
|
+
## SDK and MCP
|
|
13
|
+
|
|
14
|
+
Use Parse MCP when an agent is discovering or calling APIs ad hoc, or managing
|
|
15
|
+
account-owned server-side variants. Use this SDK for durable, typed Python
|
|
16
|
+
clients committed to an application. Neither requires the other: the SDK can
|
|
17
|
+
use `parse search` and `parse add --marketplace <listing_id>` without creating
|
|
18
|
+
an MCP subscription.
|
|
19
|
+
|
|
10
20
|
## Quickstart
|
|
11
21
|
|
|
12
22
|
In a uv-managed project (`uv init` first if you don't have a `pyproject.toml`):
|
|
@@ -16,9 +26,10 @@ uv add parse-sdk # installs the runtime + `parse` CLI into your proje
|
|
|
16
26
|
uv run parse init # prompts for your API key, then scaffolds + syncs parse_apis
|
|
17
27
|
```
|
|
18
28
|
|
|
19
|
-
Non-interactive (CI, agents, scripts):
|
|
20
|
-
`uv run parse login --api-key
|
|
21
|
-
|
|
29
|
+
Non-interactive (CI, agents, scripts): export `PARSE_API_KEY`, then run
|
|
30
|
+
`uv run parse init`. `parse login --api-key` persists a key, but is a less-safe
|
|
31
|
+
automation fallback because argv can be recorded in process listings and shell
|
|
32
|
+
history.
|
|
22
33
|
|
|
23
34
|
`parse init` is the one-time setup. It:
|
|
24
35
|
|
|
@@ -44,8 +55,9 @@ except RateLimitError:
|
|
|
44
55
|
...
|
|
45
56
|
```
|
|
46
57
|
|
|
47
|
-
(The exact methods are generated from *your* API's spec —
|
|
48
|
-
and `example.py` document each
|
|
58
|
+
(The exact methods are generated from *your* API's spec —
|
|
59
|
+
`parse_apis/src/parse_apis/<slug>/README.md` and `example.py` document each
|
|
60
|
+
client's real surface.)
|
|
49
61
|
|
|
50
62
|
```bash
|
|
51
63
|
uv run python app.py # run your app in the project environment
|
|
@@ -69,16 +81,17 @@ they just `import parse_apis`.
|
|
|
69
81
|
|
|
70
82
|
```text
|
|
71
83
|
parse_apis/
|
|
84
|
+
AGENTS.md CLAUDE.md # committed, byte-identical agent pointers
|
|
72
85
|
pyproject.toml # committed: name="parse-apis", pinned parse-sdk==X
|
|
73
86
|
src/parse_apis/
|
|
74
87
|
__init__.py # committed scaffold — re-exports runtime errors
|
|
75
88
|
py.typed # committed
|
|
76
|
-
|
|
89
|
+
reddit_com_api/__init__.py # generated (gitignored)
|
|
77
90
|
_manifest.json # generated (gitignored)
|
|
78
91
|
AGENTS.md CLAUDE.md # generated cross-API index (gitignored)
|
|
79
92
|
```
|
|
80
93
|
|
|
81
|
-
Commit the
|
|
94
|
+
Commit the five scaffold files; the generated payload is per-key (it reveals your API
|
|
82
95
|
inventory) and stays gitignored. `parse sync` builds the next payload in a staging tree,
|
|
83
96
|
self-tests it (secret scan, import-model, no-stale-docs), and promotes it with an atomic
|
|
84
97
|
directory swap — a failed sync never leaves broken or secret-bearing files in your project.
|
|
@@ -108,14 +121,51 @@ succeeds on a clean checkout.
|
|
|
108
121
|
|
|
109
122
|
| Command | What it does |
|
|
110
123
|
| --- | --- |
|
|
111
|
-
| `parse login` | Save
|
|
112
|
-
| `parse init` | One-time: ensure creds, scaffold `parse_apis`, record deps + gitignore, first sync. |
|
|
113
|
-
| `parse sync` | Regenerate the typed payload into `parse_apis/src/parse_apis` (staged + atomic). |
|
|
114
|
-
| `parse clean` | Remove generated payload; keep the committed scaffold. |
|
|
124
|
+
| `parse login` | Save browser OAuth credentials by default, or an API key with `--api-key`. No project changes. |
|
|
125
|
+
| `parse init` | One-time: ensure creds, scaffold `parse_apis`, record deps + gitignore, first sync. Pass slugs to track just those APIs. |
|
|
115
126
|
| `parse list` | Print the APIs the current key can call (no generation). |
|
|
127
|
+
| `parse sync` | Regenerate the typed payload into `parse_apis/src/parse_apis` (staged + atomic). `--check` is a CI dry run. |
|
|
128
|
+
| `parse add` | Add APIs to the repo's synced set (`--marketplace` for shared canonicals; new entries pin the latest usable release, while re-add preserves existing pin/live state), then reconcile. |
|
|
129
|
+
| `parse remove` | Stop syncing APIs (account or marketplace) and prune their generated clients. |
|
|
130
|
+
| `parse search` | Search the Parse marketplace for ready-made APIs. |
|
|
131
|
+
| `parse clean` | Remove generated payload; keep the committed scaffold. |
|
|
116
132
|
| `parse whoami` | Show the current key + base URL. |
|
|
133
|
+
| `parse doctor` | Diagnose the project's SDK install; `--fix` applies safe remediations. |
|
|
134
|
+
| `parse help` | Guided getting-started workflow (`--json` for machine-readable orientation). |
|
|
117
135
|
|
|
118
|
-
Run `parse <cmd> --help` for full per-command help
|
|
136
|
+
Run `parse <cmd> --help` for full per-command help — every command's help ends
|
|
137
|
+
with copy-pasteable Examples.
|
|
138
|
+
|
|
139
|
+
## Agents & automation
|
|
140
|
+
|
|
141
|
+
The CLI is built to run headless: prompts only fire on an interactive terminal
|
|
142
|
+
(non-TTY runs fail fast with the exact fix), warnings go to stderr, and stdout
|
|
143
|
+
stays machine-parseable under `--json`.
|
|
144
|
+
|
|
145
|
+
**Machine-readable output.** `parse help --json` is the orientation entrypoint:
|
|
146
|
+
it lists every command with `requires_auth` / `requires_init` facts and (under
|
|
147
|
+
`agent_modes`) which commands accept `--json` — the list is derived from the
|
|
148
|
+
live command registry, so it never goes stale. The `--json` shapes are stable:
|
|
149
|
+
|
|
150
|
+
| Command | Shape |
|
|
151
|
+
| --- | --- |
|
|
152
|
+
| `whoami --json` | object: `base_url`, `api_key_masked`, `key_configured`, `key_sent_to_host`, `web_login`, `auth_method` |
|
|
153
|
+
| `list --json` | array of `{id, slug, name, modeled, endpoints, resources, version}` |
|
|
154
|
+
| `sync` / `add` / `remove` `--json` | object: `written[] {slug, id, root, status, warnings[]}`, `skipped[] {slug, id, reason}`, `removed[]`, `quarantined[] {slug, id, retained, findings[]}`, `failures[]` — on a gate abort, `{failures}` alone with exit 1; under `sync --check`, `failures` carries every finding (non-empty ⟺ exit 1) |
|
|
155
|
+
| `doctor --json` | object: `ok`, `findings[] {level, title, detail, fix, kind}` |
|
|
156
|
+
| `search --json` | array of `{id, slug, name, source_url, endpoint_count, is_authenticated}` |
|
|
157
|
+
|
|
158
|
+
**Exit codes.** `0` — success. `1` — the command ran and detected a problem
|
|
159
|
+
(sync's pre-promotion gate abort, `sync --check` findings, `doctor` error-level
|
|
160
|
+
findings, `init` refusing to migrate a legacy tree with non-Parse files, a lost
|
|
161
|
+
concurrent-promote race). `2` — the command couldn't run: usage errors, an
|
|
162
|
+
uninitialized project, missing/withheld credentials or an untrusted host,
|
|
163
|
+
network failures.
|
|
164
|
+
|
|
165
|
+
**Credentials in automation.** Prefer exporting `PARSE_API_KEY` over passing
|
|
166
|
+
`--api-key` on the command line — argv is visible in process listings and shell
|
|
167
|
+
history. `parse login` (browser OAuth by default; `--web` is explicit) requires an interactive terminal
|
|
168
|
+
and fails fast headless.
|
|
119
169
|
|
|
120
170
|
## Versioning
|
|
121
171
|
|
|
@@ -127,8 +177,8 @@ regenerate against the new runtime; `init`/`sync` restamp the pin.
|
|
|
127
177
|
|
|
128
178
|
| Var | Default | Notes |
|
|
129
179
|
| --- | --- | --- |
|
|
130
|
-
| `PARSE_API_KEY` | – | Auth for real requests (or use `parse login`). |
|
|
131
|
-
| `
|
|
180
|
+
| `PARSE_API_KEY` | – | Auth for real requests (or use `parse login`). Preferred over `--api-key` in automation. |
|
|
181
|
+
| `PARSE_ACCESS_TOKEN` | – | OAuth bearer token override (normally managed by interactive `parse login`). |
|
|
132
182
|
|
|
133
183
|
## Changelog
|
|
134
184
|
|
|
@@ -4,7 +4,7 @@ Typed clients are generated by `parse sync` into a project-local
|
|
|
4
4
|
`parse_apis/` package and imported directly:
|
|
5
5
|
|
|
6
6
|
import parse_sdk as parse # errors / runtime helpers
|
|
7
|
-
from parse_apis.
|
|
7
|
+
from parse_apis.reddit_com_api import Reddit # generated typed client
|
|
8
8
|
|
|
9
9
|
try:
|
|
10
10
|
reddit = Reddit() # picks up `parse login` creds
|
|
@@ -37,7 +37,7 @@ from parse_sdk._runtime import (
|
|
|
37
37
|
UpstreamError,
|
|
38
38
|
)
|
|
39
39
|
|
|
40
|
-
__version__ = "0.
|
|
40
|
+
__version__ = "0.2.0"
|
|
41
41
|
__all__ = [
|
|
42
42
|
"BaseAPI",
|
|
43
43
|
"Resource",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Comparison helpers for server-owned API identifiers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
_UUID_RE = re.compile(
|
|
8
|
+
r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
|
|
9
|
+
r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _identifier_key(value: str) -> str:
|
|
14
|
+
"""Return a comparison key without changing opaque identifier semantics."""
|
|
15
|
+
return value.casefold() if _UUID_RE.fullmatch(value) else value
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _same_identifier(left: object, right: object) -> bool:
|
|
19
|
+
return (
|
|
20
|
+
isinstance(left, str)
|
|
21
|
+
and isinstance(right, str)
|
|
22
|
+
and _identifier_key(left) == _identifier_key(right)
|
|
23
|
+
)
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
"""The committed, declarative project config — the ``[tool.parse]`` table in the
|
|
2
|
+
consumer ``pyproject.toml`` that records which APIs this repo wants.
|
|
3
|
+
|
|
4
|
+
This is the DESIRED-state manifest: committed, reproducible across clones/CI,
|
|
5
|
+
and the source of truth ``parse sync`` reconciles the generated tree to. It is
|
|
6
|
+
distinct from ``parse_apis/_manifest.json`` (the GENERATED uuid→slug map, which
|
|
7
|
+
is gitignored and rebuilt every sync).
|
|
8
|
+
|
|
9
|
+
* ``apis`` — the account APIs this repo wants, recorded as *server slugs* (the
|
|
10
|
+
readable, committed form). A ``scraper_id`` (UUID) is also accepted on read (a
|
|
11
|
+
tolerant dual-read), so a hand-written UUID — or any future identifier — still
|
|
12
|
+
resolves and is never mistaken for a missing entry. The key being ABSENT means
|
|
13
|
+
"all your account APIs" (the post-``init`` default); an explicit list
|
|
14
|
+
(including the empty list) means exactly those.
|
|
15
|
+
* ``marketplace`` — locally-downloaded marketplace canonicals, optionally
|
|
16
|
+
pinned to a release. Unknown fields fail closed: generated projects pin the
|
|
17
|
+
SDK version, so silently guessing at a newer config contract is unnecessary.
|
|
18
|
+
|
|
19
|
+
We OWN the ``[tool.parse]`` table: writes regenerate it wholesale from the values
|
|
20
|
+
we just read back, so a hand-edited value (e.g. a slug you added to ``apis``)
|
|
21
|
+
survives, but formatting/comments *inside* the table are normalized. We never
|
|
22
|
+
touch anything outside it.
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import json
|
|
27
|
+
import re
|
|
28
|
+
try:
|
|
29
|
+
import tomllib # Python 3.11+
|
|
30
|
+
except ModuleNotFoundError: # 3.10 — tomllib landed in 3.11; tomli is its backport
|
|
31
|
+
import tomli as tomllib # type: ignore[no-redef]
|
|
32
|
+
|
|
33
|
+
from dataclasses import dataclass, field
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
from typing import Dict, List, Optional
|
|
36
|
+
|
|
37
|
+
from parse_sdk._identifiers import _identifier_key
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class MarketplaceEntry:
|
|
42
|
+
"""A locally-downloaded marketplace canonical recorded for reproducible
|
|
43
|
+
re-sync. ``scraper_id`` (canonical_scraper_id) is the execution + schema
|
|
44
|
+
primary key; ``version`` pins execution via the ``API-Snapshot-Version``
|
|
45
|
+
header."""
|
|
46
|
+
scraper_id: str
|
|
47
|
+
slug: str
|
|
48
|
+
version: Optional[int] = None
|
|
49
|
+
listing_id: Optional[str] = None
|
|
50
|
+
source_url: Optional[str] = None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass
|
|
54
|
+
class ProjectConfig:
|
|
55
|
+
# None → the `apis` key is absent → "all your account APIs" (default).
|
|
56
|
+
# [...] → exactly these account APIs as server slugs; a scraper_id (UUID) is
|
|
57
|
+
# also accepted on read ([] = no account APIs).
|
|
58
|
+
apis: Optional[List[str]] = None
|
|
59
|
+
marketplace: List[MarketplaceEntry] = field(default_factory=list)
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def all_account_mode(self) -> bool:
|
|
63
|
+
"""True when the repo tracks ALL account APIs (apis key absent)."""
|
|
64
|
+
return self.apis is None
|
|
65
|
+
|
|
66
|
+
def account_desired(self, all_account_slugs: List[str]) -> List[str]:
|
|
67
|
+
"""The account server-slugs this repo wants, given the full account set."""
|
|
68
|
+
return list(all_account_slugs) if self.apis is None else list(self.apis)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ProjectConfigError(ValueError):
|
|
72
|
+
"""The consumer's committed ``[tool.parse]`` desired state is invalid."""
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# --------------------------------------------------------------------------- #
|
|
76
|
+
# read
|
|
77
|
+
# --------------------------------------------------------------------------- #
|
|
78
|
+
|
|
79
|
+
def read_project_config(consumer_pyproject: Path) -> ProjectConfig:
|
|
80
|
+
"""Parse ``[tool.parse]`` from the consumer pyproject.
|
|
81
|
+
|
|
82
|
+
A missing file/table means the backward-compatible all-account default.
|
|
83
|
+
Once ``[tool.parse]`` exists, malformed TOML or known fields fail closed:
|
|
84
|
+
silently widening a broken declaration to all-account can prune or generate
|
|
85
|
+
clients the user did not request.
|
|
86
|
+
"""
|
|
87
|
+
try:
|
|
88
|
+
data = tomllib.loads(consumer_pyproject.read_text())
|
|
89
|
+
except FileNotFoundError:
|
|
90
|
+
return ProjectConfig()
|
|
91
|
+
except OSError as e:
|
|
92
|
+
raise ProjectConfigError(f"could not read {consumer_pyproject}: {e}") from e
|
|
93
|
+
except tomllib.TOMLDecodeError as e:
|
|
94
|
+
raise ProjectConfigError(f"invalid TOML in {consumer_pyproject}: {e}") from e
|
|
95
|
+
|
|
96
|
+
tool_tbl = data.get("tool") if isinstance(data, dict) else None
|
|
97
|
+
if tool_tbl is None:
|
|
98
|
+
return ProjectConfig()
|
|
99
|
+
if not isinstance(tool_tbl, dict):
|
|
100
|
+
raise ProjectConfigError("[tool] must be a table")
|
|
101
|
+
if "parse" not in tool_tbl:
|
|
102
|
+
return ProjectConfig()
|
|
103
|
+
parse_tbl = tool_tbl.get("parse")
|
|
104
|
+
if not isinstance(parse_tbl, dict):
|
|
105
|
+
raise ProjectConfigError("[tool.parse] must be a table")
|
|
106
|
+
|
|
107
|
+
known_table_fields = {"apis", "marketplace"}
|
|
108
|
+
_reject_unknown_fields(parse_tbl, known_table_fields, "tool.parse")
|
|
109
|
+
|
|
110
|
+
apis: Optional[List[str]] = None
|
|
111
|
+
if "apis" in parse_tbl:
|
|
112
|
+
raw = parse_tbl.get("apis")
|
|
113
|
+
# Entries are server slugs (a scraper_id UUID is also accepted) — both are
|
|
114
|
+
# plain strings, so no narrowing is needed here; the tolerant dual-read in
|
|
115
|
+
# `cli._apply_account_selection` resolves either form.
|
|
116
|
+
if not isinstance(raw, list):
|
|
117
|
+
raise ProjectConfigError("tool.parse.apis must be an array of non-empty strings")
|
|
118
|
+
if any(not isinstance(item, str) or not item for item in raw):
|
|
119
|
+
raise ProjectConfigError("tool.parse.apis must contain only non-empty strings")
|
|
120
|
+
apis = list(raw)
|
|
121
|
+
|
|
122
|
+
marketplace: List[MarketplaceEntry] = []
|
|
123
|
+
if "marketplace" in parse_tbl:
|
|
124
|
+
raw_mkt = parse_tbl.get("marketplace")
|
|
125
|
+
if not isinstance(raw_mkt, list):
|
|
126
|
+
raise ProjectConfigError("tool.parse.marketplace must be an array of tables")
|
|
127
|
+
seen_scraper_ids: Dict[str, tuple[int, Optional[int]]] = {}
|
|
128
|
+
for i, row in enumerate(raw_mkt):
|
|
129
|
+
where = f"tool.parse.marketplace[{i}]"
|
|
130
|
+
if not isinstance(row, dict):
|
|
131
|
+
raise ProjectConfigError(f"{where} must be a table")
|
|
132
|
+
known_row_fields = {"scraper_id", "slug", "version", "listing_id", "source_url"}
|
|
133
|
+
_reject_unknown_fields(row, known_row_fields, where)
|
|
134
|
+
sid, slug = row.get("scraper_id"), row.get("slug")
|
|
135
|
+
if not isinstance(sid, str) or not sid:
|
|
136
|
+
raise ProjectConfigError(f"{where}.scraper_id must be a non-empty string")
|
|
137
|
+
if not isinstance(slug, str) or not slug:
|
|
138
|
+
raise ProjectConfigError(f"{where}.slug must be a non-empty string")
|
|
139
|
+
ver = row.get("version")
|
|
140
|
+
if ver is not None and (
|
|
141
|
+
not isinstance(ver, int) or isinstance(ver, bool) or ver < 1):
|
|
142
|
+
raise ProjectConfigError(f"{where}.version must be a positive integer")
|
|
143
|
+
sid_key = _identifier_key(sid)
|
|
144
|
+
if sid_key in seen_scraper_ids:
|
|
145
|
+
prior_index, prior_version = seen_scraper_ids[sid_key]
|
|
146
|
+
raise ProjectConfigError(
|
|
147
|
+
f"{where}.scraper_id duplicates tool.parse.marketplace"
|
|
148
|
+
f"[{prior_index}] for {sid!r} (versions {prior_version!r} and "
|
|
149
|
+
f"{ver!r}); each canonical scraper_id must be declared once"
|
|
150
|
+
)
|
|
151
|
+
seen_scraper_ids[sid_key] = (i, ver)
|
|
152
|
+
listing_id = row.get("listing_id")
|
|
153
|
+
if listing_id is not None and not isinstance(listing_id, str):
|
|
154
|
+
raise ProjectConfigError(f"{where}.listing_id must be a string")
|
|
155
|
+
source_url = row.get("source_url")
|
|
156
|
+
if source_url is not None and not isinstance(source_url, str):
|
|
157
|
+
raise ProjectConfigError(f"{where}.source_url must be a string")
|
|
158
|
+
marketplace.append(MarketplaceEntry(
|
|
159
|
+
scraper_id=sid,
|
|
160
|
+
slug=slug,
|
|
161
|
+
version=ver,
|
|
162
|
+
listing_id=listing_id,
|
|
163
|
+
source_url=source_url,
|
|
164
|
+
))
|
|
165
|
+
return ProjectConfig(apis=apis, marketplace=marketplace)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# --------------------------------------------------------------------------- #
|
|
169
|
+
# write (regenerate the table we own)
|
|
170
|
+
# --------------------------------------------------------------------------- #
|
|
171
|
+
|
|
172
|
+
def _toml_str(s: str) -> str:
|
|
173
|
+
"""A TOML basic string. Slugs/uuids are tame, but source_url is freer —
|
|
174
|
+
JSON's string escapes are also valid TOML basic-string escapes."""
|
|
175
|
+
# json.dumps handles TOML-compatible escapes for C0 controls, quotes, and
|
|
176
|
+
# backslashes. JSON permits a raw DEL character while TOML does not.
|
|
177
|
+
return json.dumps(s, ensure_ascii=False).replace("\x7f", "\\u007f")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _reject_unknown_fields(table: dict, known: set, where: str) -> None:
|
|
181
|
+
"""Fail closed on fields this pinned SDK does not understand.
|
|
182
|
+
|
|
183
|
+
The CLI owns and regenerates this table. Preserving arbitrary siblings would
|
|
184
|
+
require a complete lossless TOML editor and still could not distinguish a
|
|
185
|
+
future field from a destructive typo such as ``apiss``. A clear upgrade
|
|
186
|
+
error is smaller, deterministic, and cannot widen desired state.
|
|
187
|
+
"""
|
|
188
|
+
common_typos = {
|
|
189
|
+
"api": "apis",
|
|
190
|
+
"marketpalce": "marketplace",
|
|
191
|
+
"scrapper_id": "scraper_id",
|
|
192
|
+
"scraperid": "scraper_id",
|
|
193
|
+
"versoin": "version",
|
|
194
|
+
"listng_id": "listing_id",
|
|
195
|
+
"soruce_url": "source_url",
|
|
196
|
+
}
|
|
197
|
+
canonical_by_case = {field.casefold(): field for field in known}
|
|
198
|
+
unknown = sorted(set(table) - known)
|
|
199
|
+
if not unknown:
|
|
200
|
+
return
|
|
201
|
+
details = []
|
|
202
|
+
for key in unknown:
|
|
203
|
+
suggestion = canonical_by_case.get(key.casefold())
|
|
204
|
+
if suggestion is None:
|
|
205
|
+
candidate = common_typos.get(key.casefold())
|
|
206
|
+
suggestion = candidate if candidate in known else None
|
|
207
|
+
details.append(
|
|
208
|
+
f"{where}.{key}" + (f" (did you mean {suggestion!r}?)" if suggestion else "")
|
|
209
|
+
)
|
|
210
|
+
raise ProjectConfigError(
|
|
211
|
+
"unsupported project configuration field(s): " + ", ".join(details)
|
|
212
|
+
+ "; this project may require a newer parse-sdk — upgrade it before retrying"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def render_parse_table(config: ProjectConfig) -> str:
|
|
217
|
+
"""The full ``[tool.parse]`` block (+ any ``[[tool.parse.marketplace]]``
|
|
218
|
+
sub-tables). Trailing newline included."""
|
|
219
|
+
lines: List[str] = [
|
|
220
|
+
"# Managed by the parse CLI (`parse add` / `parse remove` / `parse sync`).",
|
|
221
|
+
"# Edit `apis` to curate which of your account APIs this repo syncs;",
|
|
222
|
+
"# an absent `apis` key means all of them.",
|
|
223
|
+
"[tool.parse]",
|
|
224
|
+
]
|
|
225
|
+
if config.apis is not None:
|
|
226
|
+
if config.apis:
|
|
227
|
+
lines.append("apis = [")
|
|
228
|
+
for slug in config.apis:
|
|
229
|
+
lines.append(f" {_toml_str(slug)},")
|
|
230
|
+
lines.append("]")
|
|
231
|
+
else:
|
|
232
|
+
lines.append("apis = []")
|
|
233
|
+
for m in config.marketplace:
|
|
234
|
+
lines.append("")
|
|
235
|
+
lines.append("[[tool.parse.marketplace]]")
|
|
236
|
+
lines.append(f"scraper_id = {_toml_str(m.scraper_id)}")
|
|
237
|
+
lines.append(f"slug = {_toml_str(m.slug)}")
|
|
238
|
+
if m.version is not None:
|
|
239
|
+
lines.append(f"version = {int(m.version)}")
|
|
240
|
+
if m.listing_id:
|
|
241
|
+
lines.append(f"listing_id = {_toml_str(m.listing_id)}")
|
|
242
|
+
if m.source_url:
|
|
243
|
+
lines.append(f"source_url = {_toml_str(m.source_url)}")
|
|
244
|
+
return "\n".join(lines) + "\n"
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _strip_parse_tables(text: str) -> str:
|
|
248
|
+
"""Remove the existing ``[tool.parse]`` / ``[tool.parse.*]`` /
|
|
249
|
+
``[[tool.parse.*]]`` blocks (header line through the line before the next
|
|
250
|
+
table header or EOF), leaving everything else byte-for-byte."""
|
|
251
|
+
out: List[str] = []
|
|
252
|
+
skipping = False
|
|
253
|
+
header_re = re.compile(r"^\s*\[\[?\s*([^\]]+?)\s*\]\]?\s*(?:#.*)?$")
|
|
254
|
+
for line in text.splitlines(keepends=True):
|
|
255
|
+
m = header_re.match(line)
|
|
256
|
+
if m:
|
|
257
|
+
name = m.group(1)
|
|
258
|
+
skipping = name == "tool.parse" or name.startswith("tool.parse.")
|
|
259
|
+
if not skipping:
|
|
260
|
+
out.append(line)
|
|
261
|
+
return "".join(out)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def write_project_config(consumer_pyproject: Path, config: ProjectConfig) -> None:
|
|
265
|
+
"""Persist ``config`` into the consumer pyproject's ``[tool.parse]`` table,
|
|
266
|
+
regenerating only that table. Raises ValueError if the result wouldn't be
|
|
267
|
+
valid TOML (invariant: we never write a broken pyproject)."""
|
|
268
|
+
original = consumer_pyproject.read_text()
|
|
269
|
+
stripped = _strip_parse_tables(original).rstrip()
|
|
270
|
+
block = render_parse_table(config)
|
|
271
|
+
new = (stripped + "\n\n" + block) if stripped else block
|
|
272
|
+
try:
|
|
273
|
+
tomllib.loads(new)
|
|
274
|
+
except tomllib.TOMLDecodeError as e: # pragma: no cover — defensive invariant
|
|
275
|
+
raise ValueError(f"refusing to write pyproject.toml — the [tool.parse] edit "
|
|
276
|
+
f"would corrupt it ({e}).") from e
|
|
277
|
+
consumer_pyproject.write_text(new)
|