parse-sdk 0.1.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.
@@ -0,0 +1,18 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ venv/
8
+ parse_stubs/
9
+ uv.lock
10
+ .coverage
11
+ .pytest_cache/
12
+ .mypy_cache/
13
+ .ruff_cache/
14
+ .dev/
15
+
16
+ # Personal working-notes / superpowers plans+specs — keep out of the shared repo.
17
+ docs/superpowers/
18
+ /plans/
@@ -0,0 +1,18 @@
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.1.0] — first public release
7
+
8
+ Initial public release of the `parse-sdk` runtime + `parse` CLI: a
9
+ dynamic-yet-typed Python SDK that generates a project-local, fully typed client
10
+ for the Parse APIs your key can call.
11
+
12
+ - **CLI** (`parse`): `init`, `sync`, `login` (API key or browser OAuth via
13
+ `--web`), `doctor` (diagnose / `--fix`), `list`, `whoami`, `help`, `clean`,
14
+ and marketplace `add` / `search` / `remove`, plus `[tool.parse]`-driven
15
+ selective sync.
16
+ - **Typed runtime**: resources, collections, pagination, retries with a
17
+ wall-clock budget, and host-bound authentication.
18
+ - Ships `py.typed`; supports Python 3.10–3.14.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Parse
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,177 @@
1
+ Metadata-Version: 2.4
2
+ Name: parse-sdk
3
+ Version: 0.1.0
4
+ Summary: Dynamic-yet-typed Python SDK for Parse APIs
5
+ Project-URL: Homepage, https://parse.bot
6
+ Project-URL: Changelog, https://pypi.org/project/parse-sdk/#history
7
+ Author-email: Parse <team@parse.bot>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: agents,api-client,codegen,parse,scraping,sdk
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: click>=8.1
23
+ Requires-Dist: httpx>=0.27
24
+ Requires-Dist: tomli>=1.1.0; python_version < '3.11'
25
+ Provides-Extra: dev
26
+ Requires-Dist: pyright>=1.1.408; extra == 'dev'
27
+ Requires-Dist: pytest>=8; extra == 'dev'
28
+ Requires-Dist: tomli>=2; (python_version < '3.11') and extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # parse-sdk
32
+
33
+ Typed Python SDK tooling for [Parse](https://parse.bot) APIs.
34
+
35
+ `parse-sdk` is the shared **runtime + CLI**. It generates a project-local
36
+ `parse_apis` package — a real, installed, editable dependency — that holds the
37
+ typed client for every API your key can call. Your application code imports from
38
+ `parse_apis`; `parse-sdk` provides the runtime bases and errors those clients use.
39
+
40
+ ## Quickstart
41
+
42
+ In a uv-managed project (`uv init` first if you don't have a `pyproject.toml`):
43
+
44
+ ```bash
45
+ uv add parse-sdk # installs the runtime + `parse` CLI into your project
46
+ uv run parse init # prompts for your API key, then scaffolds + syncs parse_apis
47
+ ```
48
+
49
+ Non-interactive (CI, agents, scripts): authenticate first with
50
+ `uv run parse login --api-key YOUR_KEY` (add `--base-url …` for a non-default
51
+ host), or export `PARSE_API_KEY` — then `uv run parse init` runs without prompts.
52
+
53
+ `parse init` is the one-time setup. It:
54
+
55
+ 1. ensures credentials (prompts and stores to `~/.config/parse/credentials`, or run `parse login` first);
56
+ 2. creates the committed `parse_apis` scaffold;
57
+ 3. records `parse_apis` as an **editable path dependency** (and a `[tool.uv.sources]` entry);
58
+ 4. appends a deny-by-default `.gitignore` block (commits the scaffold, ignores generated payload);
59
+ 5. migrates — or safely refuses — any legacy flat `parse_apis/` layout;
60
+ 6. runs the first `parse sync`.
61
+
62
+ Then use it:
63
+
64
+ ```python
65
+ from parse_apis.reddit_com_api import Reddit # generated, typed client (slugs look like <domain>_<tld>_api)
66
+ from parse_apis import RateLimitError # runtime errors, re-exported
67
+
68
+ reddit = Reddit() # picks up `parse login` credentials
69
+ try:
70
+ # Navigate from a constructible resource, with a bounded fetch:
71
+ for post in reddit.subreddit("smallbusiness").search_posts(query="hiring", limit=10):
72
+ print(post.title)
73
+ except RateLimitError:
74
+ ...
75
+ ```
76
+
77
+ (The exact methods are generated from *your* API's spec — `parse_apis/<slug>/README.md`
78
+ and `example.py` document each client's real surface.)
79
+
80
+ ```bash
81
+ uv run python app.py # run your app in the project environment
82
+ uv run parse sync # later: refresh after you change APIs in Parse
83
+ ```
84
+
85
+ ## Running commands (`uv run` vs. activation)
86
+
87
+ `uv add parse-sdk` installs the `parse` console script into your project's `.venv`,
88
+ which isn't on your shell `PATH` by default. Two equivalent ways to reach it:
89
+
90
+ - **`uv run parse <cmd>`** — runs the venv's `parse` with no activation, auto-syncing first. (Recommended.)
91
+ - **`source .venv/bin/activate`** then `parse <cmd>` directly.
92
+
93
+ Prefer one of these over a global `uv tool install parse-sdk`: codegen must run with
94
+ the **same** pinned `parse-sdk` your project runs against (see *Versioning* below), and a
95
+ global copy can drift out of sync. Your end users running their *app* never invoke `parse` —
96
+ they just `import parse_apis`.
97
+
98
+ ## On-disk layout
99
+
100
+ ```text
101
+ parse_apis/
102
+ pyproject.toml # committed: name="parse-apis", pinned parse-sdk==X
103
+ src/parse_apis/
104
+ __init__.py # committed scaffold — re-exports runtime errors
105
+ py.typed # committed
106
+ reddit/__init__.py # generated (gitignored)
107
+ _manifest.json # generated (gitignored)
108
+ AGENTS.md CLAUDE.md # generated cross-API index (gitignored)
109
+ ```
110
+
111
+ Commit the three scaffold files; the generated payload is per-key (it reveals your API
112
+ inventory) and stays gitignored. `parse sync` builds the next payload in a staging tree,
113
+ self-tests it (secret scan, import-model, no-stale-docs), and promotes it with an atomic
114
+ directory swap — a failed sync never leaves broken or secret-bearing files in your project.
115
+
116
+ ## Import model
117
+
118
+ - Application code imports generated clients from `parse_apis.<slug>`.
119
+ - Runtime errors/base types are re-exported from `parse_apis` (sourced from `parse_sdk`).
120
+ - Because `parse_apis` is an **installed** editable package, imports resolve from any
121
+ directory — no `sys.path` or editor `extraPaths` tricks.
122
+ - Generated files are project-local and differ per Parse API key.
123
+
124
+ ## Fresh clone & CI (post-publish)
125
+
126
+ ```bash
127
+ uv sync # installs deps incl. the editable parse_apis scaffold
128
+ uv run parse sync # regenerate the per-key payload
129
+ uv run pytest && uv run pyright # pyright must run against the PROJECT env —
130
+ # add both as dev deps (`uv add --dev pytest pyright`);
131
+ # a bare/global `pyright` can't resolve parse_apis
132
+ ```
133
+
134
+ The committed scaffold is a valid dependency target before any slugs exist, so `uv sync`
135
+ succeeds on a clean checkout.
136
+
137
+ ## CLI
138
+
139
+ | Command | What it does |
140
+ | --- | --- |
141
+ | `parse login` | Save API key + base URL to local credentials. No project changes. |
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. |
145
+ | `parse list` | Print the APIs the current key can call (no generation). |
146
+ | `parse whoami` | Show the current key + base URL. |
147
+
148
+ Run `parse <cmd> --help` for full per-command help.
149
+
150
+ ## Versioning
151
+
152
+ `parse_apis/pyproject.toml` pins the **exact** `parse-sdk` it was generated against — generated
153
+ code subclasses the runtime by name. After upgrading `parse-sdk`, **re-run `parse sync`** to
154
+ regenerate against the new runtime; `init`/`sync` restamp the pin.
155
+
156
+ ## Env vars
157
+
158
+ | Var | Default | Notes |
159
+ | --- | --- | --- |
160
+ | `PARSE_API_KEY` | – | Auth for real requests (or use `parse login`). |
161
+ | `PARSE_API_BASE_URL` | `https://api.parse.bot` | Locally: `http://localhost:8001`. |
162
+
163
+ ## Changelog
164
+
165
+ Release history is on the PyPI project page:
166
+ <https://pypi.org/project/parse-sdk/#history>.
167
+
168
+ ## Security
169
+
170
+ To report a security issue, email **security@parse.bot** — please do not open
171
+ a public issue.
172
+
173
+ ## License
174
+
175
+ [MIT](LICENSE) © 2026 Parse. The license covers the `parse-sdk` runtime,
176
+ codegen, and CLI; use of the Parse API/service is governed separately by
177
+ Parse's terms.
@@ -0,0 +1,147 @@
1
+ # parse-sdk
2
+
3
+ Typed Python SDK tooling for [Parse](https://parse.bot) APIs.
4
+
5
+ `parse-sdk` is the shared **runtime + CLI**. It generates a project-local
6
+ `parse_apis` package — a real, installed, editable dependency — that holds the
7
+ typed client for every API your key can call. Your application code imports from
8
+ `parse_apis`; `parse-sdk` provides the runtime bases and errors those clients use.
9
+
10
+ ## Quickstart
11
+
12
+ In a uv-managed project (`uv init` first if you don't have a `pyproject.toml`):
13
+
14
+ ```bash
15
+ uv add parse-sdk # installs the runtime + `parse` CLI into your project
16
+ uv run parse init # prompts for your API key, then scaffolds + syncs parse_apis
17
+ ```
18
+
19
+ Non-interactive (CI, agents, scripts): authenticate first with
20
+ `uv run parse login --api-key YOUR_KEY` (add `--base-url …` for a non-default
21
+ host), or export `PARSE_API_KEY` — then `uv run parse init` runs without prompts.
22
+
23
+ `parse init` is the one-time setup. It:
24
+
25
+ 1. ensures credentials (prompts and stores to `~/.config/parse/credentials`, or run `parse login` first);
26
+ 2. creates the committed `parse_apis` scaffold;
27
+ 3. records `parse_apis` as an **editable path dependency** (and a `[tool.uv.sources]` entry);
28
+ 4. appends a deny-by-default `.gitignore` block (commits the scaffold, ignores generated payload);
29
+ 5. migrates — or safely refuses — any legacy flat `parse_apis/` layout;
30
+ 6. runs the first `parse sync`.
31
+
32
+ Then use it:
33
+
34
+ ```python
35
+ from parse_apis.reddit_com_api import Reddit # generated, typed client (slugs look like <domain>_<tld>_api)
36
+ from parse_apis import RateLimitError # runtime errors, re-exported
37
+
38
+ reddit = Reddit() # picks up `parse login` credentials
39
+ try:
40
+ # Navigate from a constructible resource, with a bounded fetch:
41
+ for post in reddit.subreddit("smallbusiness").search_posts(query="hiring", limit=10):
42
+ print(post.title)
43
+ except RateLimitError:
44
+ ...
45
+ ```
46
+
47
+ (The exact methods are generated from *your* API's spec — `parse_apis/<slug>/README.md`
48
+ and `example.py` document each client's real surface.)
49
+
50
+ ```bash
51
+ uv run python app.py # run your app in the project environment
52
+ uv run parse sync # later: refresh after you change APIs in Parse
53
+ ```
54
+
55
+ ## Running commands (`uv run` vs. activation)
56
+
57
+ `uv add parse-sdk` installs the `parse` console script into your project's `.venv`,
58
+ which isn't on your shell `PATH` by default. Two equivalent ways to reach it:
59
+
60
+ - **`uv run parse <cmd>`** — runs the venv's `parse` with no activation, auto-syncing first. (Recommended.)
61
+ - **`source .venv/bin/activate`** then `parse <cmd>` directly.
62
+
63
+ Prefer one of these over a global `uv tool install parse-sdk`: codegen must run with
64
+ the **same** pinned `parse-sdk` your project runs against (see *Versioning* below), and a
65
+ global copy can drift out of sync. Your end users running their *app* never invoke `parse` —
66
+ they just `import parse_apis`.
67
+
68
+ ## On-disk layout
69
+
70
+ ```text
71
+ parse_apis/
72
+ pyproject.toml # committed: name="parse-apis", pinned parse-sdk==X
73
+ src/parse_apis/
74
+ __init__.py # committed scaffold — re-exports runtime errors
75
+ py.typed # committed
76
+ reddit/__init__.py # generated (gitignored)
77
+ _manifest.json # generated (gitignored)
78
+ AGENTS.md CLAUDE.md # generated cross-API index (gitignored)
79
+ ```
80
+
81
+ Commit the three scaffold files; the generated payload is per-key (it reveals your API
82
+ inventory) and stays gitignored. `parse sync` builds the next payload in a staging tree,
83
+ self-tests it (secret scan, import-model, no-stale-docs), and promotes it with an atomic
84
+ directory swap — a failed sync never leaves broken or secret-bearing files in your project.
85
+
86
+ ## Import model
87
+
88
+ - Application code imports generated clients from `parse_apis.<slug>`.
89
+ - Runtime errors/base types are re-exported from `parse_apis` (sourced from `parse_sdk`).
90
+ - Because `parse_apis` is an **installed** editable package, imports resolve from any
91
+ directory — no `sys.path` or editor `extraPaths` tricks.
92
+ - Generated files are project-local and differ per Parse API key.
93
+
94
+ ## Fresh clone & CI (post-publish)
95
+
96
+ ```bash
97
+ uv sync # installs deps incl. the editable parse_apis scaffold
98
+ uv run parse sync # regenerate the per-key payload
99
+ uv run pytest && uv run pyright # pyright must run against the PROJECT env —
100
+ # add both as dev deps (`uv add --dev pytest pyright`);
101
+ # a bare/global `pyright` can't resolve parse_apis
102
+ ```
103
+
104
+ The committed scaffold is a valid dependency target before any slugs exist, so `uv sync`
105
+ succeeds on a clean checkout.
106
+
107
+ ## CLI
108
+
109
+ | Command | What it does |
110
+ | --- | --- |
111
+ | `parse login` | Save API key + base URL to local credentials. No project changes. |
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. |
115
+ | `parse list` | Print the APIs the current key can call (no generation). |
116
+ | `parse whoami` | Show the current key + base URL. |
117
+
118
+ Run `parse <cmd> --help` for full per-command help.
119
+
120
+ ## Versioning
121
+
122
+ `parse_apis/pyproject.toml` pins the **exact** `parse-sdk` it was generated against — generated
123
+ code subclasses the runtime by name. After upgrading `parse-sdk`, **re-run `parse sync`** to
124
+ regenerate against the new runtime; `init`/`sync` restamp the pin.
125
+
126
+ ## Env vars
127
+
128
+ | Var | Default | Notes |
129
+ | --- | --- | --- |
130
+ | `PARSE_API_KEY` | – | Auth for real requests (or use `parse login`). |
131
+ | `PARSE_API_BASE_URL` | `https://api.parse.bot` | Locally: `http://localhost:8001`. |
132
+
133
+ ## Changelog
134
+
135
+ Release history is on the PyPI project page:
136
+ <https://pypi.org/project/parse-sdk/#history>.
137
+
138
+ ## Security
139
+
140
+ To report a security issue, email **security@parse.bot** — please do not open
141
+ a public issue.
142
+
143
+ ## License
144
+
145
+ [MIT](LICENSE) © 2026 Parse. The license covers the `parse-sdk` runtime,
146
+ codegen, and CLI; use of the Parse API/service is governed separately by
147
+ Parse's terms.
@@ -0,0 +1,57 @@
1
+ """Parse SDK engine — errors, runtime helpers, and the `parse` CLI.
2
+
3
+ Typed clients are generated by `parse sync` into a project-local
4
+ `parse_apis/` package and imported directly:
5
+
6
+ import parse_sdk as parse # errors / runtime helpers
7
+ from parse_apis.reddit import Reddit # generated typed client
8
+
9
+ try:
10
+ reddit = Reddit() # picks up `parse login` creds
11
+ for post in reddit.subreddit("smallbusiness").search_posts(query="hiring", limit=10):
12
+ print(post.title)
13
+ except parse.RateLimitError:
14
+ ...
15
+
16
+ `parse_apis` is a real on-disk namespace package (pyright-resolvable, no
17
+ import magic). `parse_sdk` no longer resolves generated submodules.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from parse_sdk._runtime import (
23
+ AuthError,
24
+ BadRequestError,
25
+ BaseAPI,
26
+ Collection,
27
+ ForbiddenError,
28
+ NotFoundError,
29
+ PaginationError,
30
+ PaginationLimitError,
31
+ Paginator,
32
+ ParseError,
33
+ QuotaExceededError,
34
+ RateLimitError,
35
+ Resource,
36
+ RequestMeta,
37
+ UpstreamError,
38
+ )
39
+
40
+ __version__ = "0.1.0"
41
+ __all__ = [
42
+ "BaseAPI",
43
+ "Resource",
44
+ "Collection",
45
+ "Paginator",
46
+ "RequestMeta",
47
+ "ParseError",
48
+ "AuthError",
49
+ "ForbiddenError",
50
+ "NotFoundError",
51
+ "BadRequestError",
52
+ "QuotaExceededError",
53
+ "RateLimitError",
54
+ "UpstreamError",
55
+ "PaginationError",
56
+ "PaginationLimitError",
57
+ ]
@@ -0,0 +1,51 @@
1
+ """Shared parsing for the closed type-label grammar codegen emits
2
+ (``str`` | ``List[X]`` | ``Optional[X]`` | ``Dict[K, V]`` | ``_Paginator[X]`` |
3
+ unions). Single source: ``codegen_reconcile``, ``sample_gate``, and the
4
+ runtime previously each hand-rolled a splitter/unwrapper and drifted
5
+ (``Paginator[`` vs ``_Paginator[``). Stdlib-only — ``_runtime`` imports it."""
6
+ from __future__ import annotations
7
+
8
+ from typing import List, Optional
9
+
10
+ _WRAPPERS = ("List[", "Optional[", "_Paginator[")
11
+
12
+
13
+ def strip_wrapper(s: str, prefix: str) -> Optional[str]:
14
+ """The inner of a single ``<prefix>[...]`` layer, or ``None`` if ``s`` is not
15
+ that shape: ``strip_wrapper('Optional[X]', 'Optional[')`` → ``'X'``. The
16
+ one-layer primitive that ``unwrap_label`` loops over and ``sample_gate``
17
+ applies to a known prefix — single source for the ``s[len(prefix):-1]``
18
+ slice so the grammar parsers cannot drift."""
19
+ if s.startswith(prefix) and s.endswith("]"):
20
+ return s[len(prefix):-1].strip()
21
+ return None
22
+
23
+
24
+ def split_top(s: str) -> List[str]:
25
+ """Split on commas at bracket-depth 0: ``'str, Dict[a, b]'`` →
26
+ ``['str', 'Dict[a, b]']``."""
27
+ parts, depth, start = [], 0, 0
28
+ for i, ch in enumerate(s):
29
+ if ch == "[":
30
+ depth += 1
31
+ elif ch == "]":
32
+ depth -= 1
33
+ elif ch == "," and depth == 0:
34
+ parts.append(s[start:i].strip())
35
+ start = i + 1
36
+ parts.append(s[start:].strip())
37
+ return parts
38
+
39
+
40
+ def unwrap_label(s: str, *, max_depth: int = 5) -> str:
41
+ """Strip wrapper layers down to the leaf: ``'List[Optional[X]]'`` → ``'X'``."""
42
+ s = s.strip()
43
+ for _ in range(max_depth):
44
+ for pfx in _WRAPPERS:
45
+ inner = strip_wrapper(s, pfx)
46
+ if inner is not None:
47
+ s = inner
48
+ break
49
+ else:
50
+ return s
51
+ return s