semble-api 0.0.1__tar.gz → 0.0.2__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.
- {semble_api-0.0.1 → semble_api-0.0.2}/PKG-INFO +21 -8
- {semble_api-0.0.1 → semble_api-0.0.2}/README.md +19 -4
- semble_api-0.0.2/docs/agent-surfaces.md +74 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/pyproject.toml +2 -4
- {semble_api-0.0.1 → semble_api-0.0.2}/src/semble/_client.py +62 -22
- {semble_api-0.0.1 → semble_api-0.0.2}/src/semble/types.py +2 -4
- {semble_api-0.0.1 → semble_api-0.0.2}/tests/test_client.py +23 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/uv.lock +5 -475
- {semble_api-0.0.1 → semble_api-0.0.2}/.gitignore +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/.pre-commit-config.yaml +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/LICENSE +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/justfile +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/scripts/roundtrip.py +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/src/semble/__init__.py +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/src/semble/_exceptions.py +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/src/semble/_utils.py +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/src/semble/cli.py +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/src/semble/mcp.py +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/src/semble/py.typed +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/src/semble/records.py +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/src/semble/resources/__init__.py +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/src/semble/resources/_base.py +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/src/semble/resources/actors.py +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/src/semble/resources/cards.py +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/src/semble/resources/collections.py +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/src/semble/resources/connections.py +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/src/semble/resources/feeds.py +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/src/semble/resources/graph.py +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/src/semble/resources/notifications.py +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/src/semble/resources/search.py +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/src/semble/settings.py +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/tests/__init__.py +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/tests/conftest.py +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/tests/test_cli.py +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/tests/test_mcp.py +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/tests/test_resources.py +0 -0
- {semble_api-0.0.1 → semble_api-0.0.2}/tests/test_types.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: semble-api
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.2
|
|
4
4
|
Summary: python client for the semble api
|
|
5
5
|
Author-email: zzstoatzz <thrast36@gmail.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -10,14 +10,12 @@ Classifier: Development Status :: 3 - Alpha
|
|
|
10
10
|
Classifier: Intended Audience :: Developers
|
|
11
11
|
Classifier: License :: OSI Approved :: MIT License
|
|
12
12
|
Classifier: Programming Language :: Python :: 3
|
|
13
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
15
13
|
Classifier: Programming Language :: Python :: 3.12
|
|
16
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
17
15
|
Classifier: Programming Language :: Python :: 3.14
|
|
18
16
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
17
|
Classifier: Typing :: Typed
|
|
20
|
-
Requires-Python: >=3.
|
|
18
|
+
Requires-Python: >=3.12
|
|
21
19
|
Requires-Dist: httpx2>=2.3.0
|
|
22
20
|
Requires-Dist: pydantic-settings>=2.14.1
|
|
23
21
|
Requires-Dist: pydantic>=2.7
|
|
@@ -134,13 +132,27 @@ semble feed --pretty
|
|
|
134
132
|
|
|
135
133
|
the `mcp` extra ships a `semble-mcp` entry point that exposes this sdk to mcp clients via [fastmcp code mode](https://gofastmcp.com/servers/transforms/code-mode): three meta-tools (`search` / `get_schema` / `execute`) instead of one tool per endpoint, with model-written python composing sdk calls in a [monty](https://github.com/pydantic/monty) sandbox. intermediate results stay in the sandbox; only the final answer returns to the model's context.
|
|
136
134
|
|
|
135
|
+
create an api key at [semble.so/settings/api-keys](https://semble.so/settings/api-keys), then:
|
|
136
|
+
|
|
137
137
|
```bash
|
|
138
|
-
claude mcp add semble -- uvx --from 'semble-api[mcp]' semble-mcp
|
|
139
|
-
|
|
140
|
-
|
|
138
|
+
claude mcp add semble -e SEMBLE_API_KEY=your-key -- uvx --from 'semble-api[mcp]' semble-mcp
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
for other mcp clients (claude desktop, cursor, ...), the equivalent json config:
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"mcpServers": {
|
|
146
|
+
"semble": {
|
|
147
|
+
"command": "uvx",
|
|
148
|
+
"args": ["--from", "semble-api[mcp]", "semble-mcp"],
|
|
149
|
+
"env": { "SEMBLE_API_KEY": "your-key" }
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
141
153
|
```
|
|
142
154
|
|
|
143
|
-
|
|
155
|
+
the key is optional — without it the server is limited to public reads. the server also picks up `SEMBLE_API_KEY` from the environment or a `.env` in the working directory, so inside a checkout of this repo a plain `claude mcp add semble -- uv run --directory /path/to/this/repo semble-mcp` works too.
|
|
144
156
|
|
|
145
157
|
## examples
|
|
146
158
|
|
|
@@ -160,6 +172,7 @@ just check # ty
|
|
|
160
172
|
|
|
161
173
|
## see also
|
|
162
174
|
|
|
175
|
+
- [semble for agents](docs/agent-surfaces.md) — choosing between the sdk, cli, and mcp surfaces when wiring up agents
|
|
163
176
|
- [semble api docs](https://docs.cosmik.network/semble-api)
|
|
164
177
|
- [@semble.so/api](https://npmx.dev/package/@semble.so/api) — official typescript client
|
|
165
178
|
- [tangled.org/pdewey.com/semble](https://tangled.org/pdewey.com/semble) — go client
|
|
@@ -105,13 +105,27 @@ semble feed --pretty
|
|
|
105
105
|
|
|
106
106
|
the `mcp` extra ships a `semble-mcp` entry point that exposes this sdk to mcp clients via [fastmcp code mode](https://gofastmcp.com/servers/transforms/code-mode): three meta-tools (`search` / `get_schema` / `execute`) instead of one tool per endpoint, with model-written python composing sdk calls in a [monty](https://github.com/pydantic/monty) sandbox. intermediate results stay in the sandbox; only the final answer returns to the model's context.
|
|
107
107
|
|
|
108
|
+
create an api key at [semble.so/settings/api-keys](https://semble.so/settings/api-keys), then:
|
|
109
|
+
|
|
108
110
|
```bash
|
|
109
|
-
claude mcp add semble -- uvx --from 'semble-api[mcp]' semble-mcp
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
claude mcp add semble -e SEMBLE_API_KEY=your-key -- uvx --from 'semble-api[mcp]' semble-mcp
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
for other mcp clients (claude desktop, cursor, ...), the equivalent json config:
|
|
115
|
+
|
|
116
|
+
```json
|
|
117
|
+
{
|
|
118
|
+
"mcpServers": {
|
|
119
|
+
"semble": {
|
|
120
|
+
"command": "uvx",
|
|
121
|
+
"args": ["--from", "semble-api[mcp]", "semble-mcp"],
|
|
122
|
+
"env": { "SEMBLE_API_KEY": "your-key" }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
112
126
|
```
|
|
113
127
|
|
|
114
|
-
|
|
128
|
+
the key is optional — without it the server is limited to public reads. the server also picks up `SEMBLE_API_KEY` from the environment or a `.env` in the working directory, so inside a checkout of this repo a plain `claude mcp add semble -- uv run --directory /path/to/this/repo semble-mcp` works too.
|
|
115
129
|
|
|
116
130
|
## examples
|
|
117
131
|
|
|
@@ -131,6 +145,7 @@ just check # ty
|
|
|
131
145
|
|
|
132
146
|
## see also
|
|
133
147
|
|
|
148
|
+
- [semble for agents](docs/agent-surfaces.md) — choosing between the sdk, cli, and mcp surfaces when wiring up agents
|
|
134
149
|
- [semble api docs](https://docs.cosmik.network/semble-api)
|
|
135
150
|
- [@semble.so/api](https://npmx.dev/package/@semble.so/api) — official typescript client
|
|
136
151
|
- [tangled.org/pdewey.com/semble](https://tangled.org/pdewey.com/semble) — go client
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# semble for agents
|
|
2
|
+
|
|
3
|
+
this package ships three surfaces for the same api: a python sdk, a cli, and an mcp server. they share one auth story (`SEMBLE_API_KEY` from the environment or a `.env`; without it, public reads only) and one underlying client — the differences are about *where your agent runs and what it can touch*. this doc is for people wiring semble into agents, not for people developing this repo.
|
|
4
|
+
|
|
5
|
+
## at a glance
|
|
6
|
+
|
|
7
|
+
| surface | install | the agent is... |
|
|
8
|
+
| ------- | ------- | --------------- |
|
|
9
|
+
| sdk | `uv add semble-api` | python you're writing (scripts, pydantic-ai tools, pipelines) |
|
|
10
|
+
| cli | `uv add 'semble-api[cli]'` / `uvx` | something with a shell (claude code, codex, a cron job) |
|
|
11
|
+
| mcp | `claude mcp add semble -e SEMBLE_API_KEY=... -- uvx --from 'semble-api[mcp]' semble-mcp` | an mcp client, especially one without a shell (claude desktop, cursor) |
|
|
12
|
+
|
|
13
|
+
## the sdk
|
|
14
|
+
|
|
15
|
+
the substrate everything else is built on. typed sync/async clients over all ~50 `network.cosmik.*` endpoints, pydantic models on every response, snake_case in / camelCase on the wire, and an escape hatch (`client.get(nsid, params)`) for anything unwrapped.
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from semble import Semble
|
|
19
|
+
|
|
20
|
+
with Semble() as client:
|
|
21
|
+
for hit in client.search.semantic("agent memory", limit=5):
|
|
22
|
+
print(hit.url)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
use it when the agent *is* your code: tool functions in an agent framework, batch curation jobs, anything where you want validation errors before the network and real types after it. this is also the surface to build new surfaces from — the mcp server below is ~40 lines over it.
|
|
26
|
+
|
|
27
|
+
## the cli
|
|
28
|
+
|
|
29
|
+
`semble` is machine-readable by default, which makes it an agent tool as much as a human one: lists are ndjson, single results are one json object, keys are the api's camelCase. no flags needed to make it parseable — `--pretty` is the opt-in for humans, not the other way around.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
semble search "durable execution" | jq -r '.url'
|
|
33
|
+
semble add https://example.com --note "worth a read"
|
|
34
|
+
semble library pdewey.com
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
if your agent already has shell access, this is usually the right surface: zero integration work, composes with `jq`/`xargs`/everything, and each invocation is a fresh process with no session to manage. an agent that can run bash needs nothing else to read and write semble.
|
|
38
|
+
|
|
39
|
+
## the mcp server
|
|
40
|
+
|
|
41
|
+
`semble-mcp` exposes the sdk to mcp clients via [fastmcp code mode](https://gofastmcp.com/servers/transforms/code-mode). instead of one tool per endpoint (which puts ~50 schemas in the model's context before any work happens), clients see three meta-tools:
|
|
42
|
+
|
|
43
|
+
- `search` — find sdk methods by keyword
|
|
44
|
+
- `get_schema` — fetch parameter schemas for chosen methods
|
|
45
|
+
- `execute` — run model-written python in a [monty](https://github.com/pydantic/monty) sandbox, composing methods via `await call_tool("cards_search", {...})`
|
|
46
|
+
|
|
47
|
+
the payoff is composition without context cost: a workflow like "semantic search, then fetch who saved each hit, then summarize" is one `execute` block and one result in context, instead of n tool round-trips each hauling intermediate json through the model. params use the sdk's snake_case names (discover them via `get_schema` — don't guess), and mistakes come back as precise pydantic validation errors the model can fix in-loop, before anything hits the network.
|
|
48
|
+
|
|
49
|
+
prefer it when:
|
|
50
|
+
|
|
51
|
+
- the client has no shell (claude desktop, cursor, most hosted agents) — mcp is the only door, and three tools beat fifty
|
|
52
|
+
- workflows are composition-heavy and context economy matters, even if a shell exists
|
|
53
|
+
- you want the tool surface to track the sdk automatically — the server reflects over the client, so new sdk methods appear without anyone maintaining tool definitions
|
|
54
|
+
|
|
55
|
+
know the caveats: mcp client quality varies a lot in practice (see [mcpval](https://dev-log.prefect.io/mcpval/) — judge a server by what your client can actually accomplish with it, not by what it exposes), and the server is stdio + env-var auth today, launched per-user by the client rather than hosted.
|
|
56
|
+
|
|
57
|
+
## choosing
|
|
58
|
+
|
|
59
|
+
the short version: **shell → cli, your own python → sdk, neither → mcp.** code mode is the exception that can earn its place alongside a shell, when multi-call composition would otherwise drag intermediate results through context.
|
|
60
|
+
|
|
61
|
+
one thing that is *not* a differentiator: capability. all three surfaces carry the full read/write power of the api key behind them. pick by runtime fit, not by trying to use the surface as a permission boundary — if you want an agent restricted to reads, give it no key (public reads work) rather than a narrower surface.
|
|
62
|
+
|
|
63
|
+
## writes are public
|
|
64
|
+
|
|
65
|
+
semble is a social knowledge graph on atproto: cards, notes, collections, and connections your agent creates are real records, visible to the network and attributed to the account behind the key. that's the point — shared curation trails are what make the graph worth reading — but it means an agent writing to semble is publishing, not journaling.
|
|
66
|
+
|
|
67
|
+
a few norms keep the graph high-signal:
|
|
68
|
+
|
|
69
|
+
- write with intent: a url with a note saying *why* beats ten bare urls
|
|
70
|
+
- curate into collections rather than dumping into the library root
|
|
71
|
+
- connections (`supports`, `opposes`, `explains`, ...) are claims — make them when the relationship is real, not to inflate linkage
|
|
72
|
+
- cleanup is part of write access: `remove_from_library`, `collections.delete`, and friends work the same as the adds
|
|
73
|
+
|
|
74
|
+
for the wider picture of agents as curators, see [semble + agents](https://nate.leaflet.pub/3mnxnia4lvk2n).
|
|
@@ -4,7 +4,7 @@ dynamic = ["version"]
|
|
|
4
4
|
description = "python client for the semble api"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [{ name = "zzstoatzz", email = "thrast36@gmail.com" }]
|
|
7
|
-
requires-python = ">=3.
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
8
|
license = "MIT"
|
|
9
9
|
|
|
10
10
|
keywords = ["semble", "atproto", "bookmarks", "curation", "knowledge-graph"]
|
|
@@ -14,8 +14,6 @@ classifiers = [
|
|
|
14
14
|
"Intended Audience :: Developers",
|
|
15
15
|
"License :: OSI Approved :: MIT License",
|
|
16
16
|
"Programming Language :: Python :: 3",
|
|
17
|
-
"Programming Language :: Python :: 3.10",
|
|
18
|
-
"Programming Language :: Python :: 3.11",
|
|
19
17
|
"Programming Language :: Python :: 3.12",
|
|
20
18
|
"Programming Language :: Python :: 3.13",
|
|
21
19
|
"Programming Language :: Python :: 3.14",
|
|
@@ -111,4 +109,4 @@ exclude = [
|
|
|
111
109
|
]
|
|
112
110
|
|
|
113
111
|
[tool.ty.environment]
|
|
114
|
-
python-version = "3.
|
|
112
|
+
python-version = "3.12"
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
from types import TracebackType
|
|
2
|
-
from typing import Any
|
|
2
|
+
from typing import Any, overload
|
|
3
3
|
|
|
4
4
|
import httpx2 as httpx
|
|
5
|
-
from pydantic import SecretStr
|
|
5
|
+
from pydantic import BaseModel, SecretStr
|
|
6
6
|
|
|
7
|
-
from semble._exceptions import status_error
|
|
7
|
+
from semble._exceptions import SembleError, status_error
|
|
8
8
|
from semble.resources.actors import Actors, AsyncActors
|
|
9
9
|
from semble.resources.cards import AsyncCards, Cards
|
|
10
10
|
from semble.resources.collections import AsyncCollections, Collections
|
|
@@ -16,6 +16,25 @@ from semble.resources.search import AsyncSearch, Search
|
|
|
16
16
|
from semble.settings import SembleSettings
|
|
17
17
|
|
|
18
18
|
|
|
19
|
+
@overload
|
|
20
|
+
def _parse[T: BaseModel](response: httpx.Response, cast_to: type[T]) -> T: ...
|
|
21
|
+
@overload
|
|
22
|
+
def _parse(response: httpx.Response, cast_to: None) -> Any: ...
|
|
23
|
+
def _parse[T: BaseModel](response: httpx.Response, cast_to: type[T] | None) -> Any:
|
|
24
|
+
if not response.is_success:
|
|
25
|
+
raise status_error(response)
|
|
26
|
+
if not response.content:
|
|
27
|
+
if cast_to is not None:
|
|
28
|
+
raise SembleError(
|
|
29
|
+
f"expected a json body from {response.request.url}, got an empty response"
|
|
30
|
+
)
|
|
31
|
+
return None
|
|
32
|
+
data = response.json()
|
|
33
|
+
if cast_to is None:
|
|
34
|
+
return data
|
|
35
|
+
return cast_to.model_validate(data)
|
|
36
|
+
|
|
37
|
+
|
|
19
38
|
class _BaseClient:
|
|
20
39
|
def __init__(
|
|
21
40
|
self,
|
|
@@ -42,17 +61,6 @@ class _BaseClient:
|
|
|
42
61
|
headers["x-api-key"] = self.api_key.get_secret_value()
|
|
43
62
|
return headers
|
|
44
63
|
|
|
45
|
-
@staticmethod
|
|
46
|
-
def _parse(response: httpx.Response, cast_to: Any) -> Any:
|
|
47
|
-
if not response.is_success:
|
|
48
|
-
raise status_error(response)
|
|
49
|
-
if not response.content:
|
|
50
|
-
return None
|
|
51
|
-
data = response.json()
|
|
52
|
-
if cast_to is None:
|
|
53
|
-
return data
|
|
54
|
-
return cast_to.model_validate(data)
|
|
55
|
-
|
|
56
64
|
|
|
57
65
|
class Semble(_BaseClient):
|
|
58
66
|
"""synchronous client for the semble api.
|
|
@@ -88,29 +96,45 @@ class Semble(_BaseClient):
|
|
|
88
96
|
self.notifications = Notifications(self)
|
|
89
97
|
self.search = Search(self)
|
|
90
98
|
|
|
99
|
+
@overload
|
|
100
|
+
def get[T: BaseModel](
|
|
101
|
+
self, nsid: str, params: dict[str, Any] | None = None, *, cast_to: type[T]
|
|
102
|
+
) -> T: ...
|
|
103
|
+
@overload
|
|
91
104
|
def get(
|
|
105
|
+
self, nsid: str, params: dict[str, Any] | None = None, *, cast_to: None = None
|
|
106
|
+
) -> Any: ...
|
|
107
|
+
def get[T: BaseModel](
|
|
92
108
|
self,
|
|
93
109
|
nsid: str,
|
|
94
110
|
params: dict[str, Any] | None = None,
|
|
95
111
|
*,
|
|
96
|
-
cast_to:
|
|
112
|
+
cast_to: type[T] | None = None,
|
|
97
113
|
) -> Any:
|
|
98
114
|
"""GET an xrpc query by nsid. escape hatch for unwrapped endpoints."""
|
|
99
115
|
response = self._http.get(
|
|
100
116
|
self._url(nsid), params=params, headers=self._headers()
|
|
101
117
|
)
|
|
102
|
-
return
|
|
118
|
+
return _parse(response, cast_to)
|
|
103
119
|
|
|
120
|
+
@overload
|
|
121
|
+
def post[T: BaseModel](
|
|
122
|
+
self, nsid: str, json: dict[str, Any] | None = None, *, cast_to: type[T]
|
|
123
|
+
) -> T: ...
|
|
124
|
+
@overload
|
|
104
125
|
def post(
|
|
126
|
+
self, nsid: str, json: dict[str, Any] | None = None, *, cast_to: None = None
|
|
127
|
+
) -> Any: ...
|
|
128
|
+
def post[T: BaseModel](
|
|
105
129
|
self,
|
|
106
130
|
nsid: str,
|
|
107
131
|
json: dict[str, Any] | None = None,
|
|
108
132
|
*,
|
|
109
|
-
cast_to:
|
|
133
|
+
cast_to: type[T] | None = None,
|
|
110
134
|
) -> Any:
|
|
111
135
|
"""POST an xrpc procedure by nsid. escape hatch for unwrapped endpoints."""
|
|
112
136
|
response = self._http.post(self._url(nsid), json=json, headers=self._headers())
|
|
113
|
-
return
|
|
137
|
+
return _parse(response, cast_to)
|
|
114
138
|
|
|
115
139
|
def close(self) -> None:
|
|
116
140
|
if self._owns_http:
|
|
@@ -162,31 +186,47 @@ class AsyncSemble(_BaseClient):
|
|
|
162
186
|
self.notifications = AsyncNotifications(self)
|
|
163
187
|
self.search = AsyncSearch(self)
|
|
164
188
|
|
|
189
|
+
@overload
|
|
190
|
+
async def get[T: BaseModel](
|
|
191
|
+
self, nsid: str, params: dict[str, Any] | None = None, *, cast_to: type[T]
|
|
192
|
+
) -> T: ...
|
|
193
|
+
@overload
|
|
165
194
|
async def get(
|
|
195
|
+
self, nsid: str, params: dict[str, Any] | None = None, *, cast_to: None = None
|
|
196
|
+
) -> Any: ...
|
|
197
|
+
async def get[T: BaseModel](
|
|
166
198
|
self,
|
|
167
199
|
nsid: str,
|
|
168
200
|
params: dict[str, Any] | None = None,
|
|
169
201
|
*,
|
|
170
|
-
cast_to:
|
|
202
|
+
cast_to: type[T] | None = None,
|
|
171
203
|
) -> Any:
|
|
172
204
|
"""GET an xrpc query by nsid. escape hatch for unwrapped endpoints."""
|
|
173
205
|
response = await self._http.get(
|
|
174
206
|
self._url(nsid), params=params, headers=self._headers()
|
|
175
207
|
)
|
|
176
|
-
return
|
|
208
|
+
return _parse(response, cast_to)
|
|
177
209
|
|
|
210
|
+
@overload
|
|
211
|
+
async def post[T: BaseModel](
|
|
212
|
+
self, nsid: str, json: dict[str, Any] | None = None, *, cast_to: type[T]
|
|
213
|
+
) -> T: ...
|
|
214
|
+
@overload
|
|
178
215
|
async def post(
|
|
216
|
+
self, nsid: str, json: dict[str, Any] | None = None, *, cast_to: None = None
|
|
217
|
+
) -> Any: ...
|
|
218
|
+
async def post[T: BaseModel](
|
|
179
219
|
self,
|
|
180
220
|
nsid: str,
|
|
181
221
|
json: dict[str, Any] | None = None,
|
|
182
222
|
*,
|
|
183
|
-
cast_to:
|
|
223
|
+
cast_to: type[T] | None = None,
|
|
184
224
|
) -> Any:
|
|
185
225
|
"""POST an xrpc procedure by nsid. escape hatch for unwrapped endpoints."""
|
|
186
226
|
response = await self._http.post(
|
|
187
227
|
self._url(nsid), json=json, headers=self._headers()
|
|
188
228
|
)
|
|
189
|
-
return
|
|
229
|
+
return _parse(response, cast_to)
|
|
190
230
|
|
|
191
231
|
async def close(self) -> None:
|
|
192
232
|
if self._owns_http:
|
|
@@ -7,7 +7,7 @@ never break parsing.
|
|
|
7
7
|
"""
|
|
8
8
|
|
|
9
9
|
from datetime import datetime
|
|
10
|
-
from typing import Any,
|
|
10
|
+
from typing import Any, Literal
|
|
11
11
|
|
|
12
12
|
from pydantic import BaseModel, ConfigDict, Field, model_validator
|
|
13
13
|
from pydantic.alias_generators import to_camel
|
|
@@ -220,8 +220,6 @@ class CountResponse(Model):
|
|
|
220
220
|
unread_count: int | None = None
|
|
221
221
|
|
|
222
222
|
|
|
223
|
-
ItemT = TypeVar("ItemT")
|
|
224
|
-
|
|
225
223
|
# the api names its result array differently per endpoint (cards, users,
|
|
226
224
|
# activities, ...). collect whichever is present into `items` so every
|
|
227
225
|
# paginated response has one stable shape.
|
|
@@ -240,7 +238,7 @@ _ITEM_KEYS = (
|
|
|
240
238
|
)
|
|
241
239
|
|
|
242
240
|
|
|
243
|
-
class Page
|
|
241
|
+
class Page[ItemT](Model):
|
|
244
242
|
items: list[ItemT] = Field(default_factory=list)
|
|
245
243
|
pagination: Pagination | None = None
|
|
246
244
|
sorting: Any = None
|
|
@@ -9,11 +9,34 @@ from semble import (
|
|
|
9
9
|
NotFoundError,
|
|
10
10
|
RateLimitError,
|
|
11
11
|
Semble,
|
|
12
|
+
SembleError,
|
|
12
13
|
ServerError,
|
|
13
14
|
)
|
|
15
|
+
from semble.types import CountResponse
|
|
14
16
|
from tests.conftest import AsyncClientFactory, SyncClientFactory
|
|
15
17
|
|
|
16
18
|
|
|
19
|
+
def empty_response_client() -> Semble:
|
|
20
|
+
def handler(request: httpx.Request) -> httpx.Response:
|
|
21
|
+
return httpx.Response(200)
|
|
22
|
+
|
|
23
|
+
return Semble(
|
|
24
|
+
api_key="sk_test",
|
|
25
|
+
http_client=httpx.Client(transport=httpx.MockTransport(handler)),
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def test_empty_body_without_cast_to_returns_none() -> None:
|
|
30
|
+
assert empty_response_client().post("network.cosmik.card.updateNote", {}) is None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_empty_body_with_cast_to_raises() -> None:
|
|
34
|
+
with pytest.raises(SembleError, match="expected a json body"):
|
|
35
|
+
empty_response_client().get(
|
|
36
|
+
"network.cosmik.notification.getUnreadCount", cast_to=CountResponse
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
17
40
|
def test_api_key_header(sync_client: SyncClientFactory) -> None:
|
|
18
41
|
client, recorder = sync_client({"count": 0})
|
|
19
42
|
client.notifications.get_unread_count()
|