You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: python/README.md
+77-2Lines changed: 77 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -16,8 +16,83 @@ The pinned generator includes a temporary [pnpm patch](./patches/README.md) adap
16
16
17
17
Like the Node.js client, `codegen` generates sources and `check:generated` checks them. Commit regenerated files alongside spec, config, or generator changes. The check regenerates the package and fails if generated files were added, removed, or changed, ignoring Python bytecode caches.
18
18
19
-
`src/hackmd_api/__init__.py`is the handwritten package entry point; it currently re-exports the generated `Sdk`. Everything under `src/hackmd_api/generated/` belongs to the generator. Future handwritten wrappers must live outside that directory so regeneration cannot overwrite them. This mirrors `nodejs/src/index.ts` and `nodejs/src/generated/`, with the extra `hackmd_api` directory providing the Python package namespace.
19
+
`src/hackmd_api/__init__.py`exports the handwritten `API`, generated `Sdk`, and generated `models`. Everything under `src/hackmd_api/generated/` belongs to the generator; the custom layer lives in `api.py`. This mirrors `nodejs/src/index.ts` and `nodejs/src/generated/`, with the extra `hackmd_api` directory providing the Python package namespace.
20
20
21
21
The smoke test imports the generated package and checks the operation count against the spec, reaction enum values, personal/team paths, query parameters, JSON/Pydantic bodies, and raw responses (including 204, 304, 207, and 404). It uses HTTPX MockTransport, a custom base URL, and a fake bearer token: no network requests or real credentials are used. This is representative coverage, not verification of every operation against a live server.
22
22
23
-
The SDK still returns `httpx.Response`; callers read `.json()` or explicitly call `.raise_for_status()`. Authentication is configured on an injected `httpx.Client`, not generated from security schemes. Grouped parameters, multipart uploads, automatic response parsing, and production readiness remain outside this experiment.
23
+
## Custom API
24
+
25
+
Run local examples with `PYTHONPATH=src uv run python your_example.py`; this experiment is not yet packaged for installation.
26
+
27
+
```python
28
+
import os
29
+
from hackmd_api importAPI, models
30
+
31
+
with API(os.environ["HACKMD_ACCESS_TOKEN"]) as api:
32
+
notes = api.list_notes()
33
+
if notes:
34
+
note = api.get_note(notes[0].id)
35
+
print(note.title, note.content)
36
+
37
+
# All generated operations remain accessible; raw returns httpx.Response.
38
+
response = api.raw.list_webhooks()
39
+
response.raise_for_status()
40
+
```
41
+
42
+
The first wrapper slice covers profile, teams, history, personal notes CRUD/images, personal folders CRUD/order, and team note detail. Methods use snake_case and generated Pydantic models, not a second set of handwritten DTOs. Other operations remain on `api.raw`.
43
+
44
+
Unlike Node.js's compile-time-only types, Python parses and validates response bodies. A response that disagrees with the spec raises a Pydantic `ValidationError`; it is not silently coerced into an untyped dictionary. Generated named scalar schemas use `RootModel` (for example, `folder.name.root`). Request models with aliases can be constructed using wire names via `models.CreateUserFolderBody.model_validate({"name": "Folder", "parentFolderId": "..."})`.
45
+
46
+
The following snippets belong inside the `with API(...) as api:` block above.
47
+
48
+
```python
49
+
# Mutating example: only run against an account you intend to modify.
50
+
created = api.create_note(models.CreateNote(title="Example", content="# Hello"))
`API(token, base_url="https://api-stage.hackmd.io/v1", timeout=30, retries=3)` owns its HTTPX client. Timeout and retry delay are in seconds. Only reads, PUT, and DELETE retry transport errors, 429, or 5xx; POST/PATCH never retry automatically. Exhausted rate-limit headers stop retries. `retries=0` disables them. A retried DELETE may return 404 if the first attempt already succeeded.
65
+
66
+
HTTP errors raise `HttpResponseError` (with `code` and the original `response`), `TooManyRequestsError`, or `InternalServerError`. `wrap_response_errors=False` retains HTTPX's `HTTPStatusError`; transport errors retain their HTTPX type. Raw operations use the same authentication/base URL, but do not retry, parse, or automatically raise errors.
`get_team_note(team_path, note_id, etag=...)` behaves the same. A 304 is accepted only for conditional requests. No-content 202/204/304 returns `None`; `unwrap_data=False` preserves the original HTTPX response and status/headers. Images use `upload_note_image(note_id, image_bytes, filename="image.png", content_type="image/png")` with a multipart serializer passed to the generated operation.
80
+
81
+
## Live E2E
82
+
83
+
The suite mirrors all existing Node live scenarios (profile, lists, history, note CRUD/image, folder CRUD/nesting/order) and also checks note `200 → 304 → changed 200`. It groups dependent CRUD steps into two workflows rather than separate tests. Offline `pnpm test` never discovers or runs it.
84
+
85
+
From `python/`, reuse the ignored `nodejs/.env` (`HACKMD_ACCESS_TOKEN`, optional `HACKMD_API_ENDPOINT`). Real environment variables override values in that file:
86
+
87
+
```sh
88
+
HACKMD_E2E_MUTATIONS=0 pnpm test:e2e # read-only
89
+
HACKMD_E2E_MUTATIONS=1 pnpm test:e2e # creates/deletes notes, folders; uploads an image
90
+
```
91
+
92
+
For environment-only/CI credentials, use `uv run --frozen python tests/e2e/live.py`. Never commit a token or `.env`. Use a dedicated account, and explicitly authorize production writes before running them. Run Node and Python suites sequentially, without concurrent folder-order edits.
93
+
94
+
Resources are tracked before DTO assertions; folder order is restored before cleanup on failure. Cleanup errors fail the suite. Notes are moved to trash, **not permanently deleted**, and deleting a note does not prove its uploaded image was removed from storage. Folder endpoints unavailable on the target are reported as skips; `HACKMD_E2E_FOLDERS=0` disables folder mutations.
95
+
96
+
The shared spec allows `Team.ownerId` to be null, matching ownerless teams in production. Regression tests cover both string and null values in profiles and team lists. The current Python generator also defaults nullable fields to `None` when omitted, so it does not yet enforce the spec's required-vs-nullable distinction as strictly as the TypeScript output.
97
+
98
+
Grouped parameters, async wrappers, packaging/publication, and wrappers for all operations remain outside this first slice.
Copy file name to clipboardExpand all lines: python/patches/README.md
+8-1Lines changed: 8 additions & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -7,4 +7,11 @@
7
7
8
8
Only the shipped JavaScript bundle and HTTPX template are patched; generated output is never patched. No grouped implementation or SDK parameter-name normalization is included. Source maps remain those of the original npm release.
9
9
10
-
Remove the patch when an upstream release contains both fixes, then update the pinned version/lockfile and rerun codegen, compile, and smoke tests. PR numbers alone do not guarantee a released package contains the fixes.
10
+
Local additions for the wrapper (not part of those upstream PRs):
11
+
12
+
- Parameterized flat methods accept `request_overrides` for per-call HTTPX headers/serialization. The wrapper uses this for ETag, multipart uploads, and complete PATCH bodies without duplicating endpoint paths. `files`/`content` replaces JSON serialization.
13
+
- Pydantic bodies use `exclude_unset=True`, preserving explicit nulls without sending null for every omitted field. Raw inline optional parameters still conflate omitted values with `None`; use a full JSON body override when that distinction matters.
14
+
15
+
These are experiment-local compatibility changes, not a general multipart or unset-value implementation in the generator. Keep them until upstream provides equivalent transport options and serialization; merging the two PRs alone does not cover these additions.
16
+
17
+
Remove each part when an upstream release contains its fix, then update the pinned version/lockfile and rerun codegen, compile, and tests. PR numbers alone do not guarantee a released package contains the fixes.
0 commit comments