Resources & CRUD
Fetching
get(id, *includes)
find(**query, *includes)
Fetch a single resource by filter. Asserts exactly one result.
list()
Returns a lazy Collection — see Collections.
Creating
article = await sdk.articles.create(
title="New Post",
content="Hello",
author=admin_user, # Resource or ID
)
# POST /articles
If you save() a resource without an ID, it issues a POST (not PATCH):
article = sdk.articles._new(title="New Post", content="Hello")
# article.id is None
await article.save()
# POST /articles — creates, returns with server-assigned ID
For client-generated IDs, use create():
Available only if @api.create_one was registered.
Updating
Two equivalent forms:
refetch()
Always hits the network, replaces local state:
Deleting
Available only if @api.delete_one was registered.
Relationships
Accessing relationships
When a resource has a relationship, you access it as a regular attribute. Whether it's immediately usable depends on whether it was included.
async with sdk:
# Without include — unfetched stub
article = await sdk.articles.get(1)
# article.author is Resource(id=42) — only knows ID
# article.author.username → AttributeError
await article.author # GET /articles/1/author
print(article.author.username) # "jdoe"
# With include — fully populated
article = await sdk.articles.get(1, "author")
print(article.author.username) # "jdoe", no extra HTTP call
// Without include — unfetched stub
const article = await sdk.articles.get(1);
// article.author is Resource — only knows id=42
await article.author.fetch(); // GET /articles/1/author
console.log(article.author.username); // "jdoe"
// With include — fully populated
const article2 = await sdk.articles.get(1, "author");
console.log(article2.author.username); // "jdoe", no extra HTTP
Plural relationships
Plural relationships return a Collection instead of a single resource.
article = await sdk.articles.get(1)
# article.categories → unfetched Collection
await article.categories # GET /articles/1/categories
for category in article.categories:
print(category.name)
# With include — pre-populated
article = await sdk.articles.get(1, "categories")
for cat in article.categories:
print(cat.name)
const article = await sdk.articles.get(1);
// article.categories → unfetched Collection
await article.categories.fetch(); // GET /articles/1/categories
for (const cat of article.categories) {
console.log(cat.name);
}
// With include — pre-populated
const article2 = await sdk.articles.get(1, "categories");
for (const cat of article2.categories) {
console.log(cat.name);
}
Mutating relationships
await article.add("categories", cat1, cat2)
# POST /articles/1/relationship/categories
await article.remove("categories", cat1)
# DELETE /articles/1/relationship/categories
await article.reset("categories", cat1, cat2, cat3)
# PATCH /articles/1/relationship/categories (replaces all)
await article.edit("author", new_author)
# PATCH /articles/1/relationship/author (singular replace)
await article.add("categories", cat1, cat2);
// POST /articles/1/relationship/categories
await article.remove("categories", cat1);
// DELETE /articles/1/relationship/categories
await article.reset("categories", cat1, cat2, cat3);
// PATCH /articles/1/relationship/categories
await article.edit("author", newAuthor);
// PATCH /articles/1/relationship/author
After mutation, local relationship is invalidated. Next await fetches fresh:
Only operations with registered endpoints exist. Calling
article.add("categories", ...) without a matching server endpoint raises
AttributeError at class definition time.
RPC
Call custom actions on a resource via rpc():
The HTTP method is auto-detected from the generated _rpc_methods ClassVar.
Available actions are typed as Literal — only valid names compile.
Signature
| Param | Type | Behavior |
|---|---|---|
action |
str (typed as Literal[...] in generated code) |
RPC action name |
payload |
omitted / None |
No request body |
payload |
dict / list / scalar (no mimetype) |
Sent as JSON, auto Content-Type: application/json |
payload |
any value + mimetype set |
Sent as raw body with explicit Content-Type header |
Examples
# No payload
await article.rpc('publish')
# PUT /articles/1/publish (no body)
# JSON payload
await article.rpc('archive', {'reason': 'old'})
# POST /articles/1/archive
# Content-Type: application/json
# {"reason": "old"}
# Raw payload with custom Content-Type
await article.rpc('import', json.dumps({...}).encode(), 'application/vnd.api+json')
# POST /articles/1/import
# Content-Type: application/vnd.api+json
# {"...": ...}
// No payload
await article.rpc('publish');
// PUT /articles/1/publish (no body)
// JSON payload
await article.rpc('archive', { reason: 'old' });
// POST /articles/1/archive
// Content-Type: application/json
// {"reason": "old"}
// Raw payload with custom Content-Type
await article.rpc('import', JSON.stringify({...}), 'application/vnd.api+json');
// POST /articles/1/import
// Content-Type: application/vnd.api+json
// {"...": ...}
Response
The method tries to parse the response body as JSON. If parsing fails, it returns the raw text:
Generated code
The SDK generator emits a _rpc_methods ClassVar and a type-narrowed rpc()
override with Literal action types:
Only actions registered via @api.rpc() on the server appear in the generated
SDK. Calling an unregistered action is a type error in your IDE.