rocksky 0.1.0__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.
Files changed (53) hide show
  1. rocksky-0.1.0/.gitignore +23 -0
  2. rocksky-0.1.0/.python-version +1 -0
  3. rocksky-0.1.0/LICENSE +21 -0
  4. rocksky-0.1.0/PKG-INFO +280 -0
  5. rocksky-0.1.0/README.md +249 -0
  6. rocksky-0.1.0/examples/follow_feed.py +40 -0
  7. rocksky-0.1.0/examples/quickstart.py +41 -0
  8. rocksky-0.1.0/examples/scrobble.py +47 -0
  9. rocksky-0.1.0/examples/search.py +59 -0
  10. rocksky-0.1.0/examples/with_builder.py +61 -0
  11. rocksky-0.1.0/examples/wrapped.py +35 -0
  12. rocksky-0.1.0/pyproject.toml +73 -0
  13. rocksky-0.1.0/src/rocksky/__init__.py +89 -0
  14. rocksky-0.1.0/src/rocksky/_http.py +234 -0
  15. rocksky-0.1.0/src/rocksky/_version.py +1 -0
  16. rocksky-0.1.0/src/rocksky/builder.py +157 -0
  17. rocksky-0.1.0/src/rocksky/client.py +157 -0
  18. rocksky-0.1.0/src/rocksky/errors.py +85 -0
  19. rocksky-0.1.0/src/rocksky/models.py +412 -0
  20. rocksky-0.1.0/src/rocksky/py.typed +0 -0
  21. rocksky-0.1.0/src/rocksky/resources/__init__.py +41 -0
  22. rocksky-0.1.0/src/rocksky/resources/_base.py +49 -0
  23. rocksky-0.1.0/src/rocksky/resources/actor.py +167 -0
  24. rocksky-0.1.0/src/rocksky/resources/album.py +36 -0
  25. rocksky-0.1.0/src/rocksky/resources/apikey.py +56 -0
  26. rocksky-0.1.0/src/rocksky/resources/artist.py +85 -0
  27. rocksky-0.1.0/src/rocksky/resources/charts.py +82 -0
  28. rocksky-0.1.0/src/rocksky/resources/feed.py +90 -0
  29. rocksky-0.1.0/src/rocksky/resources/graph.py +107 -0
  30. rocksky-0.1.0/src/rocksky/resources/like.py +29 -0
  31. rocksky-0.1.0/src/rocksky/resources/mirror.py +39 -0
  32. rocksky-0.1.0/src/rocksky/resources/player.py +119 -0
  33. rocksky-0.1.0/src/rocksky/resources/playlist.py +88 -0
  34. rocksky-0.1.0/src/rocksky/resources/scrobble.py +106 -0
  35. rocksky-0.1.0/src/rocksky/resources/shout.py +102 -0
  36. rocksky-0.1.0/src/rocksky/resources/song.py +125 -0
  37. rocksky-0.1.0/src/rocksky/resources/spotify.py +32 -0
  38. rocksky-0.1.0/src/rocksky/resources/stats.py +27 -0
  39. rocksky-0.1.0/src/rocksky/resources/storage.py +51 -0
  40. rocksky-0.1.0/tests/__init__.py +0 -0
  41. rocksky-0.1.0/tests/conftest.py +36 -0
  42. rocksky-0.1.0/tests/test_actor.py +89 -0
  43. rocksky-0.1.0/tests/test_apikey.py +65 -0
  44. rocksky-0.1.0/tests/test_builder.py +123 -0
  45. rocksky-0.1.0/tests/test_charts.py +60 -0
  46. rocksky-0.1.0/tests/test_client.py +99 -0
  47. rocksky-0.1.0/tests/test_errors.py +76 -0
  48. rocksky-0.1.0/tests/test_feed.py +92 -0
  49. rocksky-0.1.0/tests/test_models.py +76 -0
  50. rocksky-0.1.0/tests/test_retries_and_hooks.py +185 -0
  51. rocksky-0.1.0/tests/test_scrobble.py +99 -0
  52. rocksky-0.1.0/tests/test_song.py +78 -0
  53. rocksky-0.1.0/uv.lock +663 -0
@@ -0,0 +1,23 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+
6
+ .venv/
7
+ .env
8
+ .env.local
9
+
10
+ build/
11
+ dist/
12
+ *.egg-info/
13
+ .eggs/
14
+
15
+ .pytest_cache/
16
+ .ruff_cache/
17
+ .mypy_cache/
18
+ .coverage
19
+ htmlcov/
20
+
21
+ .DS_Store
22
+ .idea/
23
+ .vscode/
@@ -0,0 +1 @@
1
+ 3.10
rocksky-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Tsiry Sandratraina <tsiry.sndr@rocksky.app>
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.
rocksky-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,280 @@
1
+ Metadata-Version: 2.4
2
+ Name: rocksky
3
+ Version: 0.1.0
4
+ Summary: Async Python SDK for the Rocksky XRPC API
5
+ Project-URL: Homepage, https://rocksky.app
6
+ Project-URL: Documentation, https://docs.rocksky.app
7
+ Project-URL: Repository, https://github.com/tsirysndr/rocksky
8
+ Project-URL: Issues, https://github.com/tsirysndr/rocksky/issues
9
+ Author: Rocksky
10
+ License: MIT
11
+ License-File: LICENSE
12
+ Keywords: atproto,bluesky,lastfm,music,rocksky,scrobble
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Framework :: AsyncIO
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: License :: OSI Approved :: MIT License
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Internet :: WWW/HTTP
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Classifier: Typing :: Typed
26
+ Requires-Python: >=3.10
27
+ Requires-Dist: httpx>=0.27.0
28
+ Requires-Dist: pydantic>=2.6.0
29
+ Requires-Dist: typing-extensions>=4.10.0
30
+ Description-Content-Type: text/markdown
31
+
32
+ # rocksky — Python SDK
33
+
34
+ Async Python SDK for the [Rocksky](https://rocksky.app) XRPC API.
35
+
36
+ - **Async-first** (`asyncio`, built on `httpx.AsyncClient`)
37
+ - **Typed** — Pydantic v2 models for every common entity, snake_case API
38
+ - **Pythonic** — resource-style namespaces (`client.actor`, `client.scrobble`, …)
39
+ - **Escape hatch** — `client.call(method)` for any XRPC method not yet wrapped
40
+ - Works on **Python 3.10+**
41
+
42
+ ## Install
43
+
44
+ This package is `uv`-native. From a project of your own:
45
+
46
+ ```bash
47
+ uv add rocksky
48
+ ```
49
+
50
+ Or with `pip`:
51
+
52
+ ```bash
53
+ pip install rocksky
54
+ ```
55
+
56
+ To work on the SDK itself:
57
+
58
+ ```bash
59
+ git clone https://github.com/tsirysndr/rocksky
60
+ cd rocksky/sdk/python
61
+ uv sync # creates .venv, installs runtime + dev deps
62
+ uv run pytest # run the test suite
63
+ ```
64
+
65
+ ## Quickstart
66
+
67
+ ```python
68
+ import asyncio
69
+ from rocksky import Client
70
+
71
+ async def main() -> None:
72
+ async with Client() as rocksky:
73
+ me = await rocksky.actor.get_profile("tsiry-sandratraina.com")
74
+ print(me.display_name, "—", me.did)
75
+
76
+ recent = await rocksky.scrobble.list(did=me.did, limit=10)
77
+ for s in recent:
78
+ print(f" {s.artist} — {s.title}")
79
+
80
+ asyncio.run(main())
81
+ ```
82
+
83
+ ### Authenticating
84
+
85
+ The Rocksky API uses a JWT bearer token. Pass it to the client:
86
+
87
+ ```python
88
+ async with Client(token="eyJhbGciOi…") as rocksky:
89
+ await rocksky.scrobble.create(
90
+ title="Hounds of Love",
91
+ artist="Kate Bush",
92
+ album="Hounds of Love",
93
+ duration=298000,
94
+ )
95
+ ```
96
+
97
+ You can also flip tokens mid-session:
98
+
99
+ ```python
100
+ rocksky.set_token(new_jwt)
101
+ ```
102
+
103
+ ### Self-hosting / custom base URL
104
+
105
+ Default base URL is `https://api.rocksky.app`. Override for self-hosted instances:
106
+
107
+ ```python
108
+ Client(base_url="http://localhost:8000", token=...)
109
+ ```
110
+
111
+ ### Fluent builder
112
+
113
+ If you prefer chainable configuration over a wide keyword-arg constructor —
114
+ or you want to add retries, logging hooks, or custom headers — use
115
+ `ClientBuilder`:
116
+
117
+ ```python
118
+ from rocksky import ClientBuilder
119
+
120
+ rocksky = (
121
+ ClientBuilder()
122
+ .base_url("https://api.rocksky.app")
123
+ .token(os.environ["ROCKSKY_TOKEN"])
124
+ .timeout(10.0)
125
+ .user_agent("my-app/1.0")
126
+ .header("x-request-id", "trace-abc")
127
+ .retries(3, backoff=0.5) # retry transport errors + 5xx
128
+ .on_request(lambda r: log.debug("→ %s %s", r.method, r.url))
129
+ .on_response(lambda r: log.debug("← %s", r.status_code))
130
+ .build()
131
+ )
132
+
133
+ async with rocksky:
134
+ profile = await rocksky.actor.get_profile("tsiry-sandratraina.com")
135
+ ```
136
+
137
+ A few notes:
138
+
139
+ - Every setter returns `self`, so chain freely.
140
+ - Hooks may be sync **or** async — the SDK awaits them when needed. They fire
141
+ on every attempt (useful for tracing retries).
142
+ - `retries(n)` retries on `TransportError` and any `5xx` response with
143
+ exponential backoff (`backoff * 2**attempt`). `4xx` responses are surfaced
144
+ immediately.
145
+ - `Client.builder()` is a shortcut if you only imported `Client`.
146
+ - All builder options are also available as keyword args to `Client(...)` —
147
+ the builder is sugar, not the only path.
148
+
149
+ ## Try it in IPython
150
+
151
+ The SDK is async-only, so the regular Python REPL needs `asyncio.run(...)` for every
152
+ call. IPython's autoawait is much friendlier — `await` works at the prompt:
153
+
154
+ ```bash
155
+ uv run --with ipython ipython
156
+ ```
157
+
158
+ Then:
159
+
160
+ ```python
161
+ In [1]: %autoawait
162
+ Out[1]: {'autoawait': True, 'autoawait_loop': 'asyncio'}
163
+
164
+ In [2]: from rocksky import Client
165
+
166
+ In [3]: rocksky = Client() # base_url defaults to https://api.rocksky.app
167
+
168
+ In [4]: me = await rocksky.actor.get_profile("tsiry-sandratraina.com")
169
+
170
+ In [5]: me.display_name
171
+ Out[5]: 'Tsiry Sandratraina'
172
+
173
+ In [6]: recent = await rocksky.scrobble.list(did=me.did, limit=5)
174
+
175
+ In [7]: [(s.artist, s.title) for s in recent]
176
+ Out[7]: [('Kate Bush', 'Hounds of Love'), …]
177
+
178
+ In [8]: await rocksky.aclose() # tidy up when done
179
+ ```
180
+
181
+ Jupyter notebooks behave the same — `await` works at the top level of a cell out of
182
+ the box. For other shells (`ptpython`, plain `python -m asyncio`), see your REPL's
183
+ autoawait support.
184
+
185
+ ## Resources
186
+
187
+ The client groups endpoints by namespace. Selected highlights:
188
+
189
+ | Namespace | Methods |
190
+ |-----------|---------|
191
+ | `actor` | `get_profile`, `get_albums`, `get_artists`, `get_songs`, `get_scrobbles`, `get_loved_songs`, `get_playlists`, `get_neighbours`, `get_compatibility` |
192
+ | `album` | `get`, `list`, `get_tracks` |
193
+ | `artist` | `get`, `list`, `get_albums`, `get_tracks`, `get_listeners`, `get_recent_listeners` |
194
+ | `song` | `get`, `list`, `match`, `get_recent_listeners`, `create` |
195
+ | `scrobble` | `get`, `list`, `create` |
196
+ | `charts` | `top_tracks`, `top_artists`, `scrobbles_chart` |
197
+ | `feed` | `get`, `search`, `stories`, `recommendations`, `artist_recommendations`, `album_recommendations`, `get_generator`, `list_generators` |
198
+ | `graph` | `follow`, `unfollow`, `get_followers`, `get_follows`, `get_known_followers` |
199
+ | `shout` | `create`, `reply`, `remove`, `report`, `for_profile`, `for_album`, `for_artist`, `for_track`, `replies` |
200
+ | `like` | `like_song`, `dislike_song`, `like_shout`, `dislike_shout` |
201
+ | `playlist` | `get`, `list`, `create`, `remove`, `start`, `insert_files`, `insert_directory` |
202
+ | `player` | `currently_playing`, `queue`, `play`, `pause`, `next`, `previous`, `seek`, `play_file`, `play_directory`, `add_items_to_queue`, `add_directory_to_queue` |
203
+ | `spotify` | `currently_playing`, `play`, `pause`, `next`, `previous`, `seek` |
204
+ | `apikey` | `list`, `create`, `update`, `remove` |
205
+ | `stats` | `get`, `wrapped` |
206
+ | `mirror` | `list_sources`, `put_source` |
207
+ | `dropbox` / `googledrive` | `list_files`, `get_file`, `download_file`, … |
208
+
209
+ For any endpoint that isn't wrapped (or hasn't been added yet), use the generic
210
+ escape hatch:
211
+
212
+ ```python
213
+ raw = await rocksky.call(
214
+ "app.rocksky.feed.describeFeedGenerator", verb="GET"
215
+ )
216
+ ```
217
+
218
+ ## Errors
219
+
220
+ All errors derive from `RockskyError`:
221
+
222
+ ```python
223
+ from rocksky import (
224
+ APIError,
225
+ AuthenticationError, # 401
226
+ PermissionError, # 403
227
+ NotFoundError, # 404
228
+ RateLimitError, # 429
229
+ ServerError, # 5xx
230
+ TransportError, # network / timeout
231
+ )
232
+
233
+ try:
234
+ await rocksky.song.get(uri="at://does-not-exist")
235
+ except NotFoundError as e:
236
+ print(e.status_code, e.error, e.message)
237
+ ```
238
+
239
+ `APIError` exposes `status_code`, `method`, `error`, `message`, and `body`.
240
+
241
+ ## Testing your code against the SDK
242
+
243
+ Inject your own `httpx.AsyncClient` so you can mount a mock transport:
244
+
245
+ ```python
246
+ import httpx
247
+ from rocksky import Client
248
+
249
+ transport = httpx.MockTransport(lambda req: httpx.Response(200, json={"hits": []}))
250
+ external = httpx.AsyncClient(transport=transport)
251
+
252
+ async with Client(http_client=external) as rocksky:
253
+ await rocksky.feed.search("kate bush")
254
+
255
+ await external.aclose()
256
+ ```
257
+
258
+ The SDK's own tests use [`respx`](https://lundberg.github.io/respx/) — see the
259
+ `tests/` directory for patterns.
260
+
261
+ ## Examples
262
+
263
+ Runnable example scripts live in [`examples/`](examples/):
264
+
265
+ - `examples/quickstart.py` — fetch a profile and recent scrobbles
266
+ - `examples/scrobble.py` — submit a scrobble (requires `ROCKSKY_TOKEN`)
267
+ - `examples/wrapped.py` — print someone's year-in-review summary
268
+ - `examples/search.py` — search and pretty-print hits
269
+ - `examples/follow_feed.py` — page through the follow-graph feed
270
+ - `examples/with_builder.py` — fluent builder with retries + request/response hooks
271
+
272
+ Run them with:
273
+
274
+ ```bash
275
+ uv run python examples/quickstart.py tsiry-sandratraina.com
276
+ ```
277
+
278
+ ## License
279
+
280
+ [MIT](LICENSE) © Tsiry Sandratraina.
@@ -0,0 +1,249 @@
1
+ # rocksky — Python SDK
2
+
3
+ Async Python SDK for the [Rocksky](https://rocksky.app) XRPC API.
4
+
5
+ - **Async-first** (`asyncio`, built on `httpx.AsyncClient`)
6
+ - **Typed** — Pydantic v2 models for every common entity, snake_case API
7
+ - **Pythonic** — resource-style namespaces (`client.actor`, `client.scrobble`, …)
8
+ - **Escape hatch** — `client.call(method)` for any XRPC method not yet wrapped
9
+ - Works on **Python 3.10+**
10
+
11
+ ## Install
12
+
13
+ This package is `uv`-native. From a project of your own:
14
+
15
+ ```bash
16
+ uv add rocksky
17
+ ```
18
+
19
+ Or with `pip`:
20
+
21
+ ```bash
22
+ pip install rocksky
23
+ ```
24
+
25
+ To work on the SDK itself:
26
+
27
+ ```bash
28
+ git clone https://github.com/tsirysndr/rocksky
29
+ cd rocksky/sdk/python
30
+ uv sync # creates .venv, installs runtime + dev deps
31
+ uv run pytest # run the test suite
32
+ ```
33
+
34
+ ## Quickstart
35
+
36
+ ```python
37
+ import asyncio
38
+ from rocksky import Client
39
+
40
+ async def main() -> None:
41
+ async with Client() as rocksky:
42
+ me = await rocksky.actor.get_profile("tsiry-sandratraina.com")
43
+ print(me.display_name, "—", me.did)
44
+
45
+ recent = await rocksky.scrobble.list(did=me.did, limit=10)
46
+ for s in recent:
47
+ print(f" {s.artist} — {s.title}")
48
+
49
+ asyncio.run(main())
50
+ ```
51
+
52
+ ### Authenticating
53
+
54
+ The Rocksky API uses a JWT bearer token. Pass it to the client:
55
+
56
+ ```python
57
+ async with Client(token="eyJhbGciOi…") as rocksky:
58
+ await rocksky.scrobble.create(
59
+ title="Hounds of Love",
60
+ artist="Kate Bush",
61
+ album="Hounds of Love",
62
+ duration=298000,
63
+ )
64
+ ```
65
+
66
+ You can also flip tokens mid-session:
67
+
68
+ ```python
69
+ rocksky.set_token(new_jwt)
70
+ ```
71
+
72
+ ### Self-hosting / custom base URL
73
+
74
+ Default base URL is `https://api.rocksky.app`. Override for self-hosted instances:
75
+
76
+ ```python
77
+ Client(base_url="http://localhost:8000", token=...)
78
+ ```
79
+
80
+ ### Fluent builder
81
+
82
+ If you prefer chainable configuration over a wide keyword-arg constructor —
83
+ or you want to add retries, logging hooks, or custom headers — use
84
+ `ClientBuilder`:
85
+
86
+ ```python
87
+ from rocksky import ClientBuilder
88
+
89
+ rocksky = (
90
+ ClientBuilder()
91
+ .base_url("https://api.rocksky.app")
92
+ .token(os.environ["ROCKSKY_TOKEN"])
93
+ .timeout(10.0)
94
+ .user_agent("my-app/1.0")
95
+ .header("x-request-id", "trace-abc")
96
+ .retries(3, backoff=0.5) # retry transport errors + 5xx
97
+ .on_request(lambda r: log.debug("→ %s %s", r.method, r.url))
98
+ .on_response(lambda r: log.debug("← %s", r.status_code))
99
+ .build()
100
+ )
101
+
102
+ async with rocksky:
103
+ profile = await rocksky.actor.get_profile("tsiry-sandratraina.com")
104
+ ```
105
+
106
+ A few notes:
107
+
108
+ - Every setter returns `self`, so chain freely.
109
+ - Hooks may be sync **or** async — the SDK awaits them when needed. They fire
110
+ on every attempt (useful for tracing retries).
111
+ - `retries(n)` retries on `TransportError` and any `5xx` response with
112
+ exponential backoff (`backoff * 2**attempt`). `4xx` responses are surfaced
113
+ immediately.
114
+ - `Client.builder()` is a shortcut if you only imported `Client`.
115
+ - All builder options are also available as keyword args to `Client(...)` —
116
+ the builder is sugar, not the only path.
117
+
118
+ ## Try it in IPython
119
+
120
+ The SDK is async-only, so the regular Python REPL needs `asyncio.run(...)` for every
121
+ call. IPython's autoawait is much friendlier — `await` works at the prompt:
122
+
123
+ ```bash
124
+ uv run --with ipython ipython
125
+ ```
126
+
127
+ Then:
128
+
129
+ ```python
130
+ In [1]: %autoawait
131
+ Out[1]: {'autoawait': True, 'autoawait_loop': 'asyncio'}
132
+
133
+ In [2]: from rocksky import Client
134
+
135
+ In [3]: rocksky = Client() # base_url defaults to https://api.rocksky.app
136
+
137
+ In [4]: me = await rocksky.actor.get_profile("tsiry-sandratraina.com")
138
+
139
+ In [5]: me.display_name
140
+ Out[5]: 'Tsiry Sandratraina'
141
+
142
+ In [6]: recent = await rocksky.scrobble.list(did=me.did, limit=5)
143
+
144
+ In [7]: [(s.artist, s.title) for s in recent]
145
+ Out[7]: [('Kate Bush', 'Hounds of Love'), …]
146
+
147
+ In [8]: await rocksky.aclose() # tidy up when done
148
+ ```
149
+
150
+ Jupyter notebooks behave the same — `await` works at the top level of a cell out of
151
+ the box. For other shells (`ptpython`, plain `python -m asyncio`), see your REPL's
152
+ autoawait support.
153
+
154
+ ## Resources
155
+
156
+ The client groups endpoints by namespace. Selected highlights:
157
+
158
+ | Namespace | Methods |
159
+ |-----------|---------|
160
+ | `actor` | `get_profile`, `get_albums`, `get_artists`, `get_songs`, `get_scrobbles`, `get_loved_songs`, `get_playlists`, `get_neighbours`, `get_compatibility` |
161
+ | `album` | `get`, `list`, `get_tracks` |
162
+ | `artist` | `get`, `list`, `get_albums`, `get_tracks`, `get_listeners`, `get_recent_listeners` |
163
+ | `song` | `get`, `list`, `match`, `get_recent_listeners`, `create` |
164
+ | `scrobble` | `get`, `list`, `create` |
165
+ | `charts` | `top_tracks`, `top_artists`, `scrobbles_chart` |
166
+ | `feed` | `get`, `search`, `stories`, `recommendations`, `artist_recommendations`, `album_recommendations`, `get_generator`, `list_generators` |
167
+ | `graph` | `follow`, `unfollow`, `get_followers`, `get_follows`, `get_known_followers` |
168
+ | `shout` | `create`, `reply`, `remove`, `report`, `for_profile`, `for_album`, `for_artist`, `for_track`, `replies` |
169
+ | `like` | `like_song`, `dislike_song`, `like_shout`, `dislike_shout` |
170
+ | `playlist` | `get`, `list`, `create`, `remove`, `start`, `insert_files`, `insert_directory` |
171
+ | `player` | `currently_playing`, `queue`, `play`, `pause`, `next`, `previous`, `seek`, `play_file`, `play_directory`, `add_items_to_queue`, `add_directory_to_queue` |
172
+ | `spotify` | `currently_playing`, `play`, `pause`, `next`, `previous`, `seek` |
173
+ | `apikey` | `list`, `create`, `update`, `remove` |
174
+ | `stats` | `get`, `wrapped` |
175
+ | `mirror` | `list_sources`, `put_source` |
176
+ | `dropbox` / `googledrive` | `list_files`, `get_file`, `download_file`, … |
177
+
178
+ For any endpoint that isn't wrapped (or hasn't been added yet), use the generic
179
+ escape hatch:
180
+
181
+ ```python
182
+ raw = await rocksky.call(
183
+ "app.rocksky.feed.describeFeedGenerator", verb="GET"
184
+ )
185
+ ```
186
+
187
+ ## Errors
188
+
189
+ All errors derive from `RockskyError`:
190
+
191
+ ```python
192
+ from rocksky import (
193
+ APIError,
194
+ AuthenticationError, # 401
195
+ PermissionError, # 403
196
+ NotFoundError, # 404
197
+ RateLimitError, # 429
198
+ ServerError, # 5xx
199
+ TransportError, # network / timeout
200
+ )
201
+
202
+ try:
203
+ await rocksky.song.get(uri="at://does-not-exist")
204
+ except NotFoundError as e:
205
+ print(e.status_code, e.error, e.message)
206
+ ```
207
+
208
+ `APIError` exposes `status_code`, `method`, `error`, `message`, and `body`.
209
+
210
+ ## Testing your code against the SDK
211
+
212
+ Inject your own `httpx.AsyncClient` so you can mount a mock transport:
213
+
214
+ ```python
215
+ import httpx
216
+ from rocksky import Client
217
+
218
+ transport = httpx.MockTransport(lambda req: httpx.Response(200, json={"hits": []}))
219
+ external = httpx.AsyncClient(transport=transport)
220
+
221
+ async with Client(http_client=external) as rocksky:
222
+ await rocksky.feed.search("kate bush")
223
+
224
+ await external.aclose()
225
+ ```
226
+
227
+ The SDK's own tests use [`respx`](https://lundberg.github.io/respx/) — see the
228
+ `tests/` directory for patterns.
229
+
230
+ ## Examples
231
+
232
+ Runnable example scripts live in [`examples/`](examples/):
233
+
234
+ - `examples/quickstart.py` — fetch a profile and recent scrobbles
235
+ - `examples/scrobble.py` — submit a scrobble (requires `ROCKSKY_TOKEN`)
236
+ - `examples/wrapped.py` — print someone's year-in-review summary
237
+ - `examples/search.py` — search and pretty-print hits
238
+ - `examples/follow_feed.py` — page through the follow-graph feed
239
+ - `examples/with_builder.py` — fluent builder with retries + request/response hooks
240
+
241
+ Run them with:
242
+
243
+ ```bash
244
+ uv run python examples/quickstart.py tsiry-sandratraina.com
245
+ ```
246
+
247
+ ## License
248
+
249
+ [MIT](LICENSE) © Tsiry Sandratraina.
@@ -0,0 +1,40 @@
1
+ """Page through a feed generator until exhausted or until a page cap is hit.
2
+
3
+ Run:
4
+ uv run python examples/follow_feed.py at://did:plc:.../app.rocksky.feed.generator/all
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import sys
11
+
12
+ from rocksky import Client
13
+
14
+
15
+ async def main(feed_uri: str, max_pages: int = 3) -> int:
16
+ cursor: str | None = None
17
+ seen = 0
18
+ async with Client() as rocksky:
19
+ for page in range(max_pages):
20
+ feed = await rocksky.feed.get(feed_uri, limit=25, cursor=cursor)
21
+ for item in feed.feed:
22
+ s = item.scrobble
23
+ if not s:
24
+ continue
25
+ seen += 1
26
+ stamp = s.date.isoformat() if s.date else "—"
27
+ print(f" [{stamp}] {s.user:24s} {s.artist} — {s.title}")
28
+ cursor = feed.cursor
29
+ print(f"--- end of page {page + 1}; cursor={cursor!r} ---")
30
+ if not cursor:
31
+ break
32
+ print(f"\ntotal items: {seen}")
33
+ return 0
34
+
35
+
36
+ if __name__ == "__main__":
37
+ if len(sys.argv) < 2:
38
+ print("usage: follow_feed.py <feed-at-uri>")
39
+ sys.exit(2)
40
+ sys.exit(asyncio.run(main(sys.argv[1])))
@@ -0,0 +1,41 @@
1
+ """Fetch a profile and the most recent scrobbles for a handle.
2
+
3
+ Run:
4
+ uv run python examples/quickstart.py alice.bsky.social
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import sys
11
+
12
+ from rocksky import Client, NotFoundError
13
+
14
+
15
+ async def main(handle: str) -> int:
16
+ async with Client() as rocksky:
17
+ try:
18
+ profile = await rocksky.actor.get_profile(handle)
19
+ except NotFoundError:
20
+ print(f"no profile for {handle!r}")
21
+ return 1
22
+
23
+ print(f"{profile.display_name or profile.handle} ({profile.did})")
24
+ print(f" joined: {profile.created_at}")
25
+ print()
26
+
27
+ scrobbles = await rocksky.scrobble.list(did=profile.did, limit=10)
28
+ if not scrobbles:
29
+ print("(no scrobbles yet)")
30
+ return 0
31
+
32
+ print("recent scrobbles:")
33
+ for s in scrobbles:
34
+ stamp = s.date.isoformat() if s.date else "—"
35
+ print(f" [{stamp}] {s.artist} — {s.title}")
36
+ return 0
37
+
38
+
39
+ if __name__ == "__main__":
40
+ handle = sys.argv[1] if len(sys.argv) > 1 else "tsiry.dev"
41
+ sys.exit(asyncio.run(main(handle)))
@@ -0,0 +1,47 @@
1
+ """Submit a scrobble.
2
+
3
+ Reads the bearer token from $ROCKSKY_TOKEN. Override the API base URL with
4
+ $ROCKSKY_BASE_URL if you're hitting a self-hosted instance.
5
+
6
+ Run:
7
+ ROCKSKY_TOKEN=... uv run python examples/scrobble.py
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import asyncio
13
+ import os
14
+ import sys
15
+ import time
16
+
17
+ from rocksky import APIError, Client
18
+
19
+
20
+ async def main() -> int:
21
+ token = os.environ.get("ROCKSKY_TOKEN")
22
+ if not token:
23
+ print("set $ROCKSKY_TOKEN first")
24
+ return 1
25
+
26
+ base_url = os.environ.get("ROCKSKY_BASE_URL", "https://api.rocksky.app")
27
+
28
+ async with Client(base_url=base_url, token=token) as rocksky:
29
+ try:
30
+ result = await rocksky.scrobble.create(
31
+ title="Hounds of Love",
32
+ artist="Kate Bush",
33
+ album="Hounds of Love",
34
+ duration=298_000,
35
+ year=1985,
36
+ timestamp=int(time.time()),
37
+ )
38
+ except APIError as exc:
39
+ print(f"scrobble failed: {exc}")
40
+ return 1
41
+
42
+ print("scrobble accepted:", result)
43
+ return 0
44
+
45
+
46
+ if __name__ == "__main__":
47
+ sys.exit(asyncio.run(main()))