feat(sdk-py): improve store auth type safety and docstrings (#6867)

## Summary
- Add `_StoreActionOn` class with action-specific decorator properties
(`.put`, `.get`, `.search`, `.delete`, `.list_namespaces`) to
`_StoreOn`, enabling `@auth.on.store.put` etc. which was documented but
not implemented
- Update docstring examples to show namespace-rewriting pattern as the
canonical store auth approach
- Fix `AuthContext.action` docstring: add missing `search` and `delete`
store actions
- Fix typo in `StoreSearch.query` docstring
- Add auth-handler-aware docstrings to all store TypedDicts

## Test plan
- [x] `make format && make lint` passes in `libs/sdk-py`
- [] Verify `@auth.on.store.put` decorator works at runtime

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
William FH
2026-02-19 07:16:26 -08:00
committed by GitHub
co-authored by Claude Opus 4.6
parent c4f5861166
commit 5ac837d7cd
2 changed files with 158 additions and 20 deletions
+106 -6
View File
@@ -72,8 +72,13 @@ class Auth:
assert params.get("metadata", {}).get("owner") == "allowed_user"
@auth.on.store
async def authorize_store(ctx: Auth.types.AuthContext, value: Auth.types.on):
assert ctx.user.identity in value["namespace"], "Not authorized"
async def authorize_store(ctx: Auth.types.AuthContext, value: Auth.types.on.store.value):
# Automatically scope all store operations to the user's namespace.
namespace = tuple(value["namespace"]) if value.get("namespace") else ()
assert isinstance(namespace, tuple)
if not namespace or namespace[0] != ctx.user.identity:
namespace = (ctx.user.identity, *namespace)
value["namespace"] = namespace
```
???+ note "Request Processing Flow"
@@ -170,13 +175,32 @@ class Auth:
```
Auth for the `store` resource is a bit different since its structure is developer defined.
You typically want to enforce user creds in the namespace.
You typically want to scope store operations by rewriting the namespace to include the user's identity.
The `value` dict is mutable — changes to `value["namespace"]` are used by the server for the actual operation.
```python
@auth.on.store
async def check_store_access(ctx: AuthContext, value: Auth.types.on) -> bool:
# Assuming you structure your store like (store.aput((user_id, application_context), key, value))
assert value["namespace"][0] == ctx.user.identity
async def authorize_store(ctx: AuthContext, value: Auth.types.on.store.value):
# Automatically scope all store operations to the user's namespace.
namespace = tuple(value["namespace"]) if value.get("namespace") else ()
assert isinstance(namespace, tuple)
if not namespace or namespace[0] != ctx.user.identity:
namespace = (ctx.user.identity, *namespace)
value["namespace"] = namespace
```
You can also register handlers for specific store actions:
```python
@auth.on.store.put
async def on_put(ctx: AuthContext, value: Auth.types.on.store.put.value):
# value has typed fields: namespace, key, value, index
...
@auth.on.store.get
async def on_get(ctx: AuthContext, value: Auth.types.on.store.get.value):
# value has typed fields: namespace, key
...
```
"""
# These are accessed by the API. Changes to their names or types is
@@ -483,9 +507,85 @@ class _CronsOn(
Search = types.CronsSearch
class _StoreActionOn(typing.Generic[T]):
"""Decorator for registering a handler for a specific store action."""
def __init__(
self,
auth: Auth,
action: typing.Literal["put", "get", "search", "delete", "list_namespaces"],
value: type[T],
) -> None:
self.auth = auth
self.action = action
self.value = value
def __call__(self, fn: _ActionHandler[T]) -> _ActionHandler[T]:
_validate_handler(fn)
_register_handler(self.auth, "store", self.action, fn)
return fn
class _StoreOn:
def __init__(self, auth: Auth) -> None:
self._auth = auth
self.put = _StoreActionOn(auth, "put", types.StorePut)
"""Register a handler for store put operations.
???+ example "Example"
```python
@auth.on.store.put
async def on_store_put(ctx: Auth.types.AuthContext, value: Auth.types.on.store.put.value):
# Scope puts to user's namespace
...
```
"""
self.get = _StoreActionOn(auth, "get", types.StoreGet)
"""Register a handler for store get operations.
???+ example "Example"
```python
@auth.on.store.get
async def on_store_get(ctx: Auth.types.AuthContext, value: Auth.types.on.store.get.value):
# Scope gets to user's namespace
...
```
"""
self.search = _StoreActionOn(auth, "search", types.StoreSearch)
"""Register a handler for store search operations.
???+ example "Example"
```python
@auth.on.store.search
async def on_store_search(ctx: Auth.types.AuthContext, value: Auth.types.on.store.search.value):
# Scope searches to user's namespace
...
```
"""
self.delete = _StoreActionOn(auth, "delete", types.StoreDelete)
"""Register a handler for store delete operations.
???+ example "Example"
```python
@auth.on.store.delete
async def on_store_delete(ctx: Auth.types.AuthContext, value: Auth.types.on.store.delete.value):
# Scope deletes to user's namespace
...
```
"""
self.list_namespaces = _StoreActionOn(
auth, "list_namespaces", types.StoreListNamespaces
)
"""Register a handler for store list_namespaces operations.
???+ example "Example"
```python
@auth.on.store.list_namespaces
async def on_list_ns(ctx: Auth.types.AuthContext, value: Auth.types.on.store.list_namespaces.value):
# Scope namespace listing to user's prefix
...
```
"""
@typing.overload
def __call__(
+52 -14
View File
@@ -402,7 +402,7 @@ class AuthContext(BaseAuthContext):
"list_namespaces",
]
"""The action being performed on the resource.
Most resources support the following actions:
- create: Create a new resource
- read: Read information about a resource
@@ -411,8 +411,10 @@ class AuthContext(BaseAuthContext):
- search: Search for resources
The store supports the following actions:
- put: Add or update a document in the store
- get: Get a document from the store
- put: Add or update an item in the store
- get: Get an item from the store
- search: Search for items within a namespace prefix
- delete: Delete an item from the store
- list_namespaces: List the namespaces in the store
"""
@@ -851,20 +853,34 @@ class CronsSearch(typing.TypedDict, total=False):
class StoreGet(typing.TypedDict):
"""Operation to retrieve a specific item by its namespace and key."""
"""Operation to retrieve a specific item by its namespace and key.
This dict is mutable — auth handlers can modify `namespace` to enforce
access scoping (e.g., prepending the user's identity).
"""
namespace: tuple[str, ...]
"""Hierarchical path that uniquely identifies the item's location."""
"""Hierarchical path that uniquely identifies the item's location.
Auth handlers can modify this to enforce per-user scoping.
"""
key: str
"""Unique identifier for the item within its specific namespace."""
class StoreSearch(typing.TypedDict):
"""Operation to search for items within a specified namespace hierarchy."""
"""Operation to search for items within a specified namespace hierarchy.
This dict is mutable — auth handlers can modify `namespace` to enforce
access scoping (e.g., prepending the user's identity).
"""
namespace: tuple[str, ...]
"""Prefix filter for defining the search scope."""
"""Prefix filter for defining the search scope.
Auth handlers can modify this to enforce per-user scoping.
"""
filter: dict[str, typing.Any] | None
"""Key-value pairs for filtering results based on exact matches or comparison operators."""
@@ -876,14 +892,22 @@ class StoreSearch(typing.TypedDict):
"""Number of matching items to skip for pagination."""
query: str | None
"""Naturalj language search query for semantic search capabilities."""
"""Natural language search query for semantic search capabilities."""
class StoreListNamespaces(typing.TypedDict):
"""Operation to list and filter namespaces in the store."""
"""Operation to list and filter namespaces in the store.
This dict is mutable — auth handlers can modify `namespace` (the prefix)
to enforce access scoping (e.g., prepending the user's identity).
"""
namespace: tuple[str, ...] | None
"""Prefix filter namespaces."""
"""Prefix filter for namespaces. Can be `None` if no prefix was provided.
Auth handlers can modify this to enforce per-user scoping. When `None`,
handlers should set it to `(user_id,)` to scope listing to the user's namespaces.
"""
suffix: tuple[str, ...] | None
"""Optional conditions for filtering namespaces."""
@@ -903,10 +927,17 @@ class StoreListNamespaces(typing.TypedDict):
class StorePut(typing.TypedDict):
"""Operation to store, update, or delete an item in the store."""
"""Operation to store, update, or delete an item in the store.
This dict is mutable — auth handlers can modify `namespace` to enforce
access scoping (e.g., prepending the user's identity).
"""
namespace: tuple[str, ...]
"""Hierarchical path that identifies the location of the item."""
"""Hierarchical path that identifies the location of the item.
Auth handlers can modify this to enforce per-user scoping.
"""
key: str
"""Unique identifier for the item within its namespace."""
@@ -919,10 +950,17 @@ class StorePut(typing.TypedDict):
class StoreDelete(typing.TypedDict):
"""Operation to delete an item from the store."""
"""Operation to delete an item from the store.
This dict is mutable — auth handlers can modify `namespace` to enforce
access scoping (e.g., prepending the user's identity).
"""
namespace: tuple[str, ...]
"""Hierarchical path that uniquely identifies the item's location."""
"""Hierarchical path that uniquely identifies the item's location.
Auth handlers can modify this to enforce per-user scoping.
"""
key: str
"""Unique identifier for the item within its specific namespace."""