semble-api 0.0.1__py3-none-any.whl

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/types.py ADDED
@@ -0,0 +1,268 @@
1
+ """response models and shared types for the semble api.
2
+
3
+ field names are snake_case in python and map to the api's camelCase via
4
+ pydantic aliases. enum-ish request parameters are typed as `Literal` aliases;
5
+ the same fields on response models are plain `str` so new server-side values
6
+ never break parsing.
7
+ """
8
+
9
+ from datetime import datetime
10
+ from typing import Any, Generic, Literal, TypeVar
11
+
12
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
13
+ from pydantic.alias_generators import to_camel
14
+
15
+ URLType = Literal[
16
+ "article",
17
+ "link",
18
+ "book",
19
+ "research",
20
+ "audio",
21
+ "video",
22
+ "social",
23
+ "event",
24
+ "software",
25
+ ]
26
+
27
+ AccessType = Literal["OPEN", "CLOSED"]
28
+
29
+ TargetType = Literal["USER", "COLLECTION"]
30
+
31
+ ConnectionType = Literal[
32
+ "SUPPORTS",
33
+ "OPPOSES",
34
+ "ADDRESSES",
35
+ "HELPFUL",
36
+ "LEADS_TO",
37
+ "RELATED",
38
+ "SUPPLEMENT",
39
+ "EXPLAINER",
40
+ ]
41
+
42
+ SortOrder = Literal["asc", "desc"]
43
+
44
+
45
+ class Model(BaseModel):
46
+ model_config = ConfigDict(
47
+ alias_generator=to_camel,
48
+ populate_by_name=True,
49
+ extra="allow",
50
+ )
51
+
52
+
53
+ class Pagination(Model):
54
+ current_page: int | None = None
55
+ total_pages: int | None = None
56
+ total_count: int | None = None
57
+ has_more: bool | None = None
58
+ limit: int | None = None
59
+ next_cursor: str | None = None
60
+
61
+
62
+ class User(Model):
63
+ id: str | None = None
64
+ name: str | None = None
65
+ handle: str | None = None
66
+ avatar_url: str | None = None
67
+ banner_url: str | None = None
68
+ description: str | None = None
69
+ is_following: bool | None = None
70
+ follows_you: bool | None = None
71
+ follower_count: int | None = None
72
+ following_count: int | None = None
73
+ followed_collections_count: int | None = None
74
+ url_card_count: int | None = None
75
+ collection_count: int | None = None
76
+ connection_count: int | None = None
77
+ connections_by_type: dict[str, int] | None = None
78
+ labels: list[Any] | None = None
79
+
80
+
81
+ class URLMetadata(Model):
82
+ url: str | None = None
83
+ title: str | None = None
84
+ description: str | None = None
85
+ author: str | None = None
86
+ published_date: str | None = None
87
+ site_name: str | None = None
88
+ image_url: str | None = None
89
+ type: str | None = None
90
+ retrieved_at: str | None = None
91
+ doi: str | None = None
92
+ isbn: str | None = None
93
+
94
+
95
+ class URLView(Model):
96
+ url: str | None = None
97
+ metadata: URLMetadata | None = None
98
+ url_library_count: int | None = None
99
+ url_connection_count: int | None = None
100
+ url_in_library: bool | None = None
101
+ url_is_connected: bool | None = None
102
+
103
+
104
+ class CardNote(Model):
105
+ id: str | None = None
106
+ text: str | None = None
107
+
108
+
109
+ class Collection(Model):
110
+ id: str | None = None
111
+ uri: str | None = None
112
+ name: str | None = None
113
+ author: User | None = None
114
+ description: str | None = None
115
+ access_type: str | None = None
116
+ card_count: int | None = None
117
+ created_at: datetime | None = None
118
+ updated_at: datetime | None = None
119
+ is_following: bool | None = None
120
+ follower_count: int | None = None
121
+
122
+
123
+ class URLCard(Model):
124
+ type: str | None = None
125
+ id: str | None = None
126
+ url: str | None = None
127
+ uri: str | None = None
128
+ card_content: Any = None
129
+ library_count: int | None = None
130
+ url_library_count: int | None = None
131
+ url_in_library: bool | None = None
132
+ url_is_connected: bool | None = None
133
+ created_at: datetime | None = None
134
+ updated_at: datetime | None = None
135
+ author: User | None = None
136
+ note: CardNote | None = None
137
+ collections: list[Collection] | None = None
138
+ libraries: list[User] | None = None
139
+
140
+
141
+ class Connection(Model):
142
+ id: str | None = None
143
+ type: str | None = None
144
+ note: str | None = None
145
+ created_at: datetime | None = None
146
+ updated_at: datetime | None = None
147
+ curator: User | None = None
148
+
149
+
150
+ class ConnectionView(Model):
151
+ connection: Connection
152
+ source: URLView | None = None
153
+ target: URLView | None = None
154
+
155
+
156
+ class Activity(Model):
157
+ id: str | None = None
158
+ activity_type: str | None = None
159
+ created_at: datetime | None = None
160
+ user: User | None = None
161
+ card: URLCard | None = None
162
+ collections: list[Collection] | None = None
163
+ connection: ConnectionView | None = None
164
+
165
+
166
+ class Notification(Model):
167
+ id: str | None = None
168
+ type: str | None = None
169
+ created_at: datetime | None = None
170
+ read: bool | None = None
171
+ user: User | None = None
172
+ card: URLCard | None = None
173
+ collections: list[Collection] | None = None
174
+ connection: ConnectionView | None = None
175
+ follow_target_type: str | None = None
176
+ follow_target_id: str | None = None
177
+
178
+
179
+ class NoteCard(Model):
180
+ id: str | None = None
181
+ note: str | None = None
182
+ author: User | None = None
183
+ created_at: datetime | None = None
184
+ updated_at: datetime | None = None
185
+
186
+
187
+ class LibraryEntry(Model):
188
+ user: User
189
+ card: URLCard | None = None
190
+
191
+
192
+ class URLStats(Model):
193
+ library_count: int | None = None
194
+ note_count: int | None = None
195
+ collection_count: int | None = None
196
+ connections: dict[str, dict[str, int]] | None = None
197
+
198
+
199
+ class URLMetadataResponse(Model):
200
+ metadata: URLMetadata
201
+ stats: URLStats | None = None
202
+
203
+
204
+ class AddURLResponse(Model):
205
+ url_card_id: str
206
+ note_card_id: str | None = None
207
+
208
+
209
+ class IDResponse(Model):
210
+ card_id: str | None = None
211
+ collection_id: str | None = None
212
+ connection_id: str | None = None
213
+ follow_id: str | None = None
214
+ success: bool | None = None
215
+ marked_count: int | None = None
216
+
217
+
218
+ class CountResponse(Model):
219
+ count: int | None = None
220
+ unread_count: int | None = None
221
+
222
+
223
+ ItemT = TypeVar("ItemT")
224
+
225
+ # the api names its result array differently per endpoint (cards, users,
226
+ # activities, ...). collect whichever is present into `items` so every
227
+ # paginated response has one stable shape.
228
+ _ITEM_KEYS = (
229
+ "items",
230
+ "cards",
231
+ "collections",
232
+ "users",
233
+ "urls",
234
+ "libraries",
235
+ "notes",
236
+ "activities",
237
+ "actors",
238
+ "connections",
239
+ "notifications",
240
+ )
241
+
242
+
243
+ class Page(Model, Generic[ItemT]):
244
+ items: list[ItemT] = Field(default_factory=list)
245
+ pagination: Pagination | None = None
246
+ sorting: Any = None
247
+ unread_count: int | None = None
248
+
249
+ @model_validator(mode="before")
250
+ @classmethod
251
+ def _collect_items(cls, data: Any) -> Any:
252
+ if isinstance(data, dict) and "items" not in data:
253
+ for key in _ITEM_KEYS:
254
+ if isinstance(data.get(key), list):
255
+ return {**data, "items": data[key]}
256
+ return data
257
+
258
+ def __iter__(self) -> Any:
259
+ return iter(self.items)
260
+
261
+ def __len__(self) -> int:
262
+ return len(self.items)
263
+
264
+
265
+ class CollectionDetail(Collection):
266
+ url_cards: list[URLCard] | None = None
267
+ pagination: Pagination | None = None
268
+ sorting: Any = None
@@ -0,0 +1,165 @@
1
+ Metadata-Version: 2.4
2
+ Name: semble-api
3
+ Version: 0.0.1
4
+ Summary: python client for the semble api
5
+ Author-email: zzstoatzz <thrast36@gmail.com>
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: atproto,bookmarks,curation,knowledge-graph,semble
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.10
14
+ Classifier: Programming Language :: Python :: 3.11
15
+ Classifier: Programming Language :: Python :: 3.12
16
+ Classifier: Programming Language :: Python :: 3.13
17
+ Classifier: Programming Language :: Python :: 3.14
18
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: httpx2>=2.3.0
22
+ Requires-Dist: pydantic-settings>=2.14.1
23
+ Requires-Dist: pydantic>=2.7
24
+ Provides-Extra: cli
25
+ Requires-Dist: cyclopts>=4.17.0; extra == 'cli'
26
+ Provides-Extra: mcp
27
+ Requires-Dist: fastmcp[code-mode]>=3.4.2; extra == 'mcp'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # semble-api
31
+
32
+ python client for the [semble](https://semble.so) api — collaborative bookmarking and knowledge curation on [atproto](https://atproto.com).
33
+
34
+ built on [httpx2](https://github.com/pydantic/httpx2) and [pydantic](https://docs.pydantic.dev), with sync and async clients.
35
+
36
+ ## installation
37
+
38
+ ```bash
39
+ uv add semble-api
40
+ ```
41
+
42
+ ## quick start
43
+
44
+ create an api key at [semble.so/settings/api-keys](https://semble.so/settings/api-keys), then:
45
+
46
+ ```python
47
+ from semble import Semble
48
+
49
+ client = Semble() # reads SEMBLE_API_KEY from the environment or a local .env
50
+
51
+ # add a url to your library
52
+ result = client.cards.add_url("https://example.com", note="worth a read")
53
+
54
+ # search your cards
55
+ for card in client.cards.search("durable execution"):
56
+ print(card.url)
57
+
58
+ # semantic search across semble
59
+ for hit in client.search.semantic("agent memory", threshold=0.7):
60
+ print(hit.metadata.title, hit.url)
61
+ ```
62
+
63
+ async is the same surface:
64
+
65
+ ```python
66
+ from semble import AsyncSemble
67
+
68
+ async with AsyncSemble() as client:
69
+ profile = await client.actors.get_my_profile(include_stats=True)
70
+ feed = await client.feeds.get_following(limit=25)
71
+ ```
72
+
73
+ ## api surface
74
+
75
+ resources mirror the `network.cosmik.*` xrpc namespaces:
76
+
77
+ | namespace | what's there |
78
+ | --------------------- | ------------------------------------------------------------------ |
79
+ | `client.cards` | add/search/list urls and notes, metadata, library status |
80
+ | `client.collections` | create/update/delete collections, followers, contributors |
81
+ | `client.connections` | typed links between urls (supports, opposes, explains, ...) |
82
+ | `client.feeds` | global and following activity feeds |
83
+ | `client.notifications`| list, unread count, mark read |
84
+ | `client.search` | semantic search, similar urls, account search |
85
+ | `client.actors` | profiles |
86
+ | `client.graph` | follow/unfollow users and collections |
87
+
88
+ every endpoint not yet wrapped is reachable via the escape hatch:
89
+
90
+ ```python
91
+ client.get("network.cosmik.card.getLibraryStatus", {"url": "https://example.com"})
92
+ ```
93
+
94
+ `semble.records` has pydantic models for the raw `network.cosmik.*` pds records, if you're reading or writing them directly (e.g. with [pdsx](https://github.com/zzstoatzz/pdsx)).
95
+
96
+ ## configuration
97
+
98
+ settings come from explicit kwargs, then `SEMBLE_*` environment variables, then a local `.env` file (via [pydantic-settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/)):
99
+
100
+ | setting | kwarg | default |
101
+ | ----------------- | ---------- | ----------------------------- |
102
+ | `SEMBLE_API_KEY` | `api_key` | unauthenticated (public reads work) |
103
+ | `SEMBLE_BASE_URL` | `base_url` | `https://api.semble.so/xrpc` |
104
+ | `SEMBLE_TIMEOUT` | `timeout` | `30.0` |
105
+
106
+ the api key is held as a pydantic `SecretStr`, so it won't leak into logs or reprs.
107
+
108
+ ## cli
109
+
110
+ a small [cyclopts](https://github.com/BrianPugh/cyclopts) cli ships as an extra:
111
+
112
+ ```bash
113
+ uv add 'semble-api[cli]'
114
+ # or run without installing
115
+ uvx --from 'semble-api[cli]' semble --help
116
+
117
+ semble whoami # auth sanity check
118
+ semble feed 10 --following # activity feeds
119
+ semble search "durable execution" # semantic search
120
+ semble library pdewey.com # anyone's library (or yours, with no handle)
121
+ semble add https://example.com --note "worth a read"
122
+ semble rm <card-id>
123
+ ```
124
+
125
+ output is machine-readable by default — lists are ndjson, single results are one json object, keys match the api's camelCase — so it pipes straight into jq or an agent. add `--pretty` to any command for human-formatted output:
126
+
127
+ ```bash
128
+ semble feed 25 | jq -r '.card.url'
129
+ semble search "agent memory" | jq -r '.metadata.title'
130
+ semble feed --pretty
131
+ ```
132
+
133
+ ## mcp server
134
+
135
+ 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
+
137
+ ```bash
138
+ claude mcp add semble -- uvx --from 'semble-api[mcp]' semble-mcp
139
+ # or from a checkout
140
+ claude mcp add semble -- uv run --directory /path/to/this/repo semble-mcp
141
+ ```
142
+
143
+ auth comes from `SEMBLE_API_KEY` (environment or `.env`); without a key the server is limited to public reads.
144
+
145
+ ## examples
146
+
147
+ `scripts/roundtrip.py` exercises the write paths end to end (add url → note → collection → cleanup). it mutates your real account, so run it deliberately:
148
+
149
+ ```bash
150
+ uv run scripts/roundtrip.py
151
+ ```
152
+
153
+ ## development
154
+
155
+ ```bash
156
+ just test # pytest
157
+ just fmt # ruff format + check
158
+ just check # ty
159
+ ```
160
+
161
+ ## see also
162
+
163
+ - [semble api docs](https://docs.cosmik.network/semble-api)
164
+ - [@semble.so/api](https://npmx.dev/package/@semble.so/api) — official typescript client
165
+ - [tangled.org/pdewey.com/semble](https://tangled.org/pdewey.com/semble) — go client
@@ -0,0 +1,25 @@
1
+ semble/__init__.py,sha256=vdaWtbrTsHA6GjhS7ovDBApf_6aX4W2OM-cwKJxIaCg,723
2
+ semble/_client.py,sha256=MZCS804_EF4SQ8jMigAN1akjgFnSG0f7OofeljTk9y0,6649
3
+ semble/_exceptions.py,sha256=z8Yg5bm0M7Dmu1R6184mGXxgsEXT3-kGItNhixb62dA,1550
4
+ semble/_utils.py,sha256=MoykBw6yrguVHafJPbQiQJ9Vp01Blw25FG24cXHCbmA,256
5
+ semble/cli.py,sha256=fcJ69ZysdamzIDUWuCg5l3OxqTe4vAv-YOpppzctU00,4881
6
+ semble/mcp.py,sha256=UyMELBL55kOfXH423A_fbu-GAaFCYvfRLMz_5qiq4aY,1339
7
+ semble/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ semble/records.py,sha256=VaqiG2WfWLo0MjAYCF9MNB8B_wq6I1tGlmBCJMRdLcs,2371
9
+ semble/settings.py,sha256=Wrn1zXgwsML1v8FfvOLPvnvkYupxP6FUR84Ylx5DdCY,895
10
+ semble/types.py,sha256=YFioU0N_Gy5cHeZczrQV8Z8wAWSDFchtJx7npQmLYDE,6750
11
+ semble/resources/__init__.py,sha256=3B0TbddYUJ9cjajrwS1tS7YbVMHYMOcOr48wj5crmQY,490
12
+ semble/resources/_base.py,sha256=dL6UNtRMT4C42sR9S24Qgcp6UEJPTE8Hk_N61JUQmas,313
13
+ semble/resources/actors.py,sha256=JbIVXZc3mk4bDPCcwRZvriLi4E8Hn2uEAlHMCKf2irI,1322
14
+ semble/resources/cards.py,sha256=G18KfJY8m396bTN28SM5mPrJFUvwZoOeXA99VaMOR8Y,10870
15
+ semble/resources/collections.py,sha256=4gAiuhV69NjQ02C7sEmpHXFvv2a2jWko9pPxP2Hblx8,13129
16
+ semble/resources/connections.py,sha256=0TqfYyj4hNcvXBKpNIPcz2t8B1P90Kl_9r1zBpO6gp0,6139
17
+ semble/resources/feeds.py,sha256=bmjxYD9oZXnJgRBjG43k8_Fh-2_uCGIUiSB0bycRxfE,3939
18
+ semble/resources/graph.py,sha256=XQgeRc2hgOt-fWPaHGd3pJAE4fBrrM6tsoKbpAHKFJY,5211
19
+ semble/resources/notifications.py,sha256=k0d-InWsQukYQwVLwk213VnQawcrcIo14maOYA1iBkk,2837
20
+ semble/resources/search.py,sha256=6rqq1CR0ZnapEDex9GkUI-6F5FkJmlq81l1gJ6Sxpls,3965
21
+ semble_api-0.0.1.dist-info/METADATA,sha256=YagApeDOY1YARFir8MQCp3K7m18HHiTGbigZ0Zf83dU,6453
22
+ semble_api-0.0.1.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
23
+ semble_api-0.0.1.dist-info/entry_points.txt,sha256=zF40iigk3IDcjMCOwurMf_B4iK_Zh0nzplrrCz5YobY,72
24
+ semble_api-0.0.1.dist-info/licenses/LICENSE,sha256=GCaBhyWUE9SeLzklZVA9BKgh424EOjDzofeP-9_OB-8,1066
25
+ semble_api-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ semble = semble.cli:main
3
+ semble-mcp = semble.mcp:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 zzstoatzz
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.