Sync reading position from Moon Reader app to Bookhive atproto records
atproto bookhive ereader moonreader
3
fork

Configure Feed

Select the types of activity you want to include in your feed.

Rename waggle to spacebee and genericize for public use

Package renamed (src/waggle -> src/spacebee, imports, pyproject,
Dockerfile CMD). CI no longer hardcodes git.brads.house; registry
coordinates come from REGISTRY/IMAGE_NAME repo vars and REGISTRY_USER/
REGISTRY_TOKEN secrets, and the image build is gated on `vars.REGISTRY`
so forks stay test-only by default. docker-compose.yml takes its image
tag from $IMAGE (defaults to local spacebee:latest). README, CLAUDE.md,
and .env.example scrubbed of personal hosts/handles. Dropped the empty
readest adapter stub to narrow scope to Moon+ Reader WebDAV.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

+177 -140
+5 -5
.env.example
··· 1 - # ATProto — the identity waggle writes to 2 - PDS=hermitary.brad.quest 3 - BSKY_HANDLE=bradwenner.photo 1 + # ATProto — the identity spacebee writes to 2 + PDS=bsky.social 3 + BSKY_HANDLE=you.bsky.social 4 4 BSKY_APP_PASSWORD=xxxx-xxxx-xxxx-xxxx 5 5 6 - # WebDAV access to waggle itself — what Moon+ Reader sends in Basic auth 7 - DAV_USER=brad 6 + # WebDAV access to spacebee itself — what Moon+ Reader sends in Basic auth 7 + DAV_USER=change-me 8 8 DAV_PASSWORD=change-me 9 9 10 10 # Local-disk fallback for non-.po DAV paths (Settings/, etc.)
+13 -11
.forgejo/workflows/ci.yml
··· 5 5 branches: [main] 6 6 pull_request: 7 7 8 - env: 9 - REGISTRY: git.brads.house 10 - IMAGE_NAME: brad/waggle 11 - 12 8 jobs: 13 9 test: 14 10 runs-on: docker-host ··· 34 30 35 31 image: 36 32 needs: test 37 - if: github.ref == 'refs/heads/main' 33 + if: github.ref == 'refs/heads/main' && vars.REGISTRY != '' 38 34 runs-on: docker-host 39 35 container: 40 36 image: node:22-bookworm 37 + env: 38 + # Registry coordinates come from repo/org vars+secrets so this workflow 39 + # is portable. Set REGISTRY + IMAGE_NAME as repo vars; REGISTRY_USER + 40 + # REGISTRY_TOKEN as secrets. The job is skipped if REGISTRY is unset. 41 + REGISTRY: ${{ vars.REGISTRY }} 42 + IMAGE_NAME: ${{ vars.IMAGE_NAME }} 41 43 steps: 42 44 - name: Checkout 43 45 uses: actions/checkout@v4 ··· 45 47 - name: Install Docker CLI 46 48 run: apt-get update && apt-get install -y --no-install-recommends docker.io 47 49 48 - - name: Login to Forgejo Registry 50 + - name: Login to container registry 49 51 run: | 50 - echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ${{ env.REGISTRY }} -u ${{ secrets.REGISTRY_USER }} --password-stdin 52 + echo "${{ secrets.REGISTRY_TOKEN }}" | docker login "$REGISTRY" -u "${{ secrets.REGISTRY_USER }}" --password-stdin 51 53 52 54 - name: Build and push 53 55 run: | 54 56 docker build \ 55 - -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest \ 56 - -t ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} . 57 - docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest 58 - docker push ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} 57 + -t "$REGISTRY/$IMAGE_NAME:latest" \ 58 + -t "$REGISTRY/$IMAGE_NAME:${{ github.sha }}" . 59 + docker push "$REGISTRY/$IMAGE_NAME:latest" 60 + docker push "$REGISTRY/$IMAGE_NAME:${{ github.sha }}"
+25 -27
CLAUDE.md
··· 1 - # waggle 1 + # spacebee 2 2 3 3 WebDAV shim that impersonates Moon+ Reader's sync backend and translates 4 4 requests into ATProto reads/writes on `buzz.bookhive.book` records. Protocol 5 5 adapter, not a poller — every inbound DAV request is a round-trip to the PDS. 6 6 7 - See `/Users/personal/.claude/plans/declarative-skipping-sunset.md` for the 8 - full architecture plan. 9 - 10 7 ## Shape 11 8 12 9 ``` 13 - MoonReader ─[WebDAV: PROPFIND/GET/PUT/HEAD/OPTIONS]──→ waggle ──→ ATProto PDS 10 + MoonReader ─[WebDAV: PROPFIND/GET/PUT/HEAD/OPTIONS]──→ spacebee ──→ ATProto PDS 14 11 Passthrough (Settings/, etc.) ─────────────────→ local disk at $PASSTHROUGH_ROOT 15 12 ``` 16 13 ··· 18 15 disk backing. Synthesizing the listing comes from `listRecords` on 19 16 `buzz.bookhive.book`; each record with a `bookProgress.moonReader.file` 20 17 appears as a `.po`. 21 - - Every other path hits `Passthrough` over a local filesystem. Future: 22 - annotation files route to `margin.at`, but out of scope for v1. 18 + - Every other path hits `Passthrough` over a local filesystem. Annotation 19 + routing lives outside v1. 23 20 24 21 ## Design constraints (don't re-derive) 25 22 26 23 - **Namespace**: all our extensions live under `bookProgress.moonReader.*` — 27 - never flatten into top-level lexicon fields. Same pattern survives adding 28 - `bookProgress.koReader.*` later. 24 + never flatten into top-level lexicon fields. Same pattern leaves room to 25 + add other reader namespaces (`bookProgress.koReader.*`, etc.) without 26 + collision. 29 27 - **`moonReader.position` is stored verbatim**. On GET we return it unchanged 30 - so Moon+ Reader's internal `timestamp_ms` (the ebook import mtime, see 31 - `../moon2hive/CLAUDE.md` for the forensics) stays stable. Re-synthesizing 32 - would trip Moon+ into thinking the position changed. 28 + so Moon+ Reader's internal `timestamp_ms` (the ebook's import mtime) stays 29 + stable. Re-synthesizing would trip Moon+ into thinking the position 30 + changed. 33 31 - **PUT is idempotent**: if the incoming position + filename match what's on 34 - the record, skip the write. Moon+ Reader PUTs on every pause event and most 35 - are no-ops. 32 + the record, skip the write. Moon+ Reader PUTs on every pause event and 33 + most are no-ops. 36 34 - **Finished books don't flip back to reading**: if `status` is 37 - `buzz.bookhive.defs#finished`, a PUT updates the bookProgress but keeps the 38 - status. 35 + `buzz.bookhive.defs#finished`, a PUT updates the bookProgress but keeps 36 + the status. 39 37 - **Filename = book identity for Moon+**. `moonReader.file` is the key we 40 38 look up by. If `resolve_record()` misses, fall back to fuzzy 41 39 title/author match, then catalog-search-and-create. ··· 50 48 ```sh 51 49 cp .env.example .env # fill in creds 52 50 uv sync --extra dev 53 - uv run uvicorn waggle.main:app --reload --port 8080 51 + uv run uvicorn spacebee.main:app --reload --port 8080 54 52 uv run pytest -q 55 53 uv run ruff check src tests 56 54 ``` 57 55 58 56 ## Deploy 59 57 60 - Docker image at `git.brads.house/brad/waggle`. `docker-compose.yml` is 61 - CasaOS-shaped; mount `./data:/data` for the passthrough scratch area. Put 62 - behind HTTPS reverse proxy — Moon+ Reader requires TLS for WebDAV. 58 + Single Docker container. The published `docker-compose.yml` takes the image 59 + tag from the `IMAGE` env var (defaults to `spacebee:latest`). Mount 60 + `./data:/data` for the passthrough scratch area. Put behind HTTPS reverse 61 + proxy — Moon+ Reader requires TLS for WebDAV. 63 62 64 - CI: `.forgejo/workflows/ci.yml` (test on PR; build+push image on `main`). 65 - Slim runners don't have Node.js; we `git clone` manually instead of using 66 - `actions/checkout`. 63 + CI: `.forgejo/workflows/ci.yml` runs tests on every push/PR and builds + 64 + pushes an image on `main` **only if** the `REGISTRY` repo var is set. Configure 65 + `REGISTRY` + `IMAGE_NAME` as repo/org vars and `REGISTRY_USER` + 66 + `REGISTRY_TOKEN` as secrets to enable image publishing; otherwise the job 67 + skips cleanly. 67 68 68 69 ## Related 69 70 70 - - `../moon2hive/` — the previous one-way cron script waggle replaces. The 71 - translation core in `src/waggle/atproto/bookhive.py` is lifted (with light 72 - edits) from `moon2hive.py`. 73 71 - `bookhive.buzz` — the AT Protocol book tracker that renders the records 74 - waggle writes. 72 + spacebee writes.
+1 -1
Dockerfile
··· 14 14 15 15 EXPOSE 8080 16 16 17 - CMD ["uvicorn", "waggle.main:app", "--host", "0.0.0.0", "--port", "8080"] 17 + CMD ["uvicorn", "spacebee.main:app", "--host", "0.0.0.0", "--port", "8080"]
+59 -19
README.md
··· 1 - # waggle 1 + # spacebee 2 2 3 3 A WebDAV shim that impersonates a cloud sync backend for [Moon+ Reader] and 4 4 translates its `.po` position files into ATProto reads/writes on your 5 5 [bookhive.buzz] book records. 6 6 7 - Point Moon+ Reader at `https://waggle.yourdomain/` (Basic auth). Open a book, 8 - read for a while, pause the app — your `bookProgress` on your PDS updates. Flip 9 - to `bookhive.buzz` and the progress is already there. Flip between devices and 10 - waggle hands each one the current state. 7 + Point Moon+ Reader at `https://spacebee.yourdomain/` (Basic auth). Open a 8 + book, read for a while, pause the app — your `bookProgress` on your PDS 9 + updates. Flip to `bookhive.buzz` and the progress is already there. Flip 10 + between devices and spacebee hands each one the current state. 11 11 12 12 ## How it works 13 13 14 14 ``` 15 - MoonReader ─[WebDAV: PROPFIND / GET / PUT]──→ waggle ──→ ATProto PDS 16 - (buzz.bookhive.book) 15 + MoonReader ─[WebDAV: PROPFIND / GET / PUT]──→ spacebee ──→ ATProto PDS 16 + (buzz.bookhive.book) 17 17 ``` 18 18 19 19 - `PROPFIND /Books/.Moon+/Cache/` synthesizes a directory listing from your 20 - `buzz.bookhive.book` records that have a `bookProgress.moonReader.file` field. 20 + `buzz.bookhive.book` records that have a `bookProgress.moonReader.file` 21 + field. 21 22 - `GET /Books/.Moon+/Cache/{file}.po` returns the stored position string 22 23 verbatim (preserves Moon+ Reader's internal chapter/offset encoding). 23 - - `PUT /Books/.Moon+/Cache/{file}.po` parses the `.po` body, finds the matching 24 - bookhive record (or catalog-searches and creates one), and updates 25 - `bookProgress.{percent,currentChapter,moonReader}` on your PDS. 24 + - `PUT /Books/.Moon+/Cache/{file}.po` parses the `.po` body, finds the 25 + matching bookhive record (or catalog-searches and creates one), and 26 + updates `bookProgress.{percent,currentChapter,moonReader}` on your PDS. 26 27 27 28 Non-position WebDAV paths (`Books/.Moon+/Settings/`, etc.) fall through to a 28 29 local-disk scratch area rooted at `$PASSTHROUGH_ROOT`. Those files are not 29 30 synced anywhere — they just keep Moon+ Reader happy. 30 31 32 + spacebee also serves a small read-only HTML dashboard at `/` that renders 33 + your bookhive records (currently-reading, finished, etc.). The dashboard and 34 + cover-image blob proxy at `/blob/{cid}` are public; all WebDAV endpoints are 35 + gated by HTTP Basic. 36 + 37 + ## Configuration 38 + 39 + Copy `.env.example` to `.env` and fill in: 40 + 41 + | Var | Purpose | 42 + | --- | --- | 43 + | `PDS` | Your atproto PDS host (e.g. `bsky.social`) | 44 + | `BSKY_HANDLE` | The handle spacebee writes records as | 45 + | `BSKY_APP_PASSWORD` | An [app password] for that handle | 46 + | `DAV_USER` / `DAV_PASSWORD` | Basic-auth credentials Moon+ Reader will send | 47 + | `PASSTHROUGH_ROOT` | Local-disk scratch dir for non-`.po` paths | 48 + 31 49 ## Running locally 32 50 33 51 ```sh 34 - cp .env.example .env # fill in PDS, handle, app password, DAV creds 35 - uv run uvicorn waggle.main:app --reload --port 8080 52 + cp .env.example .env 53 + uv sync --extra dev 54 + uv run uvicorn spacebee.main:app --reload --port 8080 36 55 ``` 37 56 38 57 Point a test device at `http://<your-laptop>:8080/` as the WebDAV target. 39 58 40 59 ## Deploying 41 60 42 - Single Docker container. `docker-compose.yml` is CasaOS-shaped; put waggle 43 - behind a reverse proxy with HTTPS (Moon+ Reader requires TLS for WebDAV). 61 + Single Docker container. Build locally or pull an image you've pushed to a 62 + registry, then: 63 + 64 + ```sh 65 + IMAGE=ghcr.io/you/spacebee:latest docker compose up -d 66 + ``` 67 + 68 + The provided `docker-compose.yml` takes the image tag from the `IMAGE` 69 + environment variable and defaults to `spacebee:latest` (expects a local 70 + build). Put behind a reverse proxy with HTTPS — Moon+ Reader requires TLS 71 + for WebDAV. 72 + 73 + ## CI 74 + 75 + `.forgejo/workflows/ci.yml` runs lint + tests on every push and PR. On 76 + pushes to `main` it will also build and push a Docker image, **but only if 77 + `REGISTRY` is set as a repo/org variable**. To enable image publishing, 78 + configure: 79 + 80 + - Repo/org vars: `REGISTRY` (e.g. `ghcr.io`), `IMAGE_NAME` (e.g. `you/spacebee`) 81 + - Repo/org secrets: `REGISTRY_USER`, `REGISTRY_TOKEN` 82 + 83 + If `REGISTRY` is unset, the build job is skipped and the workflow is 84 + test-only — safe to fork without needing any registry credentials. 44 85 45 86 ## Related 46 87 47 - - [`../moon2hive`](../moon2hive) — the previous one-way cron script this 48 - replaces. 49 - - [`bookhive.buzz`](https://bookhive.buzz) — the AT Protocol book tracker 50 - whose records waggle reads and writes. 88 + - [`bookhive.buzz`] — the AT Protocol book tracker whose records spacebee 89 + reads and writes. 51 90 52 91 [Moon+ Reader]: https://www.moondownload.com/ 53 92 [bookhive.buzz]: https://bookhive.buzz/ 93 + [app password]: https://bsky.app/settings/app-passwords
+5 -3
docker-compose.yml
··· 1 1 services: 2 - waggle: 3 - image: git.brads.house/brad/waggle:latest 4 - container_name: waggle 2 + spacebee: 3 + # Override with your own built image or pulled tag. 4 + # Example: `IMAGE=ghcr.io/you/spacebee:latest docker compose up -d` 5 + image: ${IMAGE:-spacebee:latest} 6 + container_name: spacebee 5 7 restart: unless-stopped 6 8 ports: 7 9 - "8080:8080"
+3 -3
pyproject.toml
··· 1 1 [project] 2 - name = "waggle" 2 + name = "spacebee" 3 3 version = "0.1.0" 4 - description = "WebDAV/KOSync shim that translates ereader sync requests into ATProto bookhive records" 4 + description = "WebDAV shim that translates Moon+ Reader sync requests into ATProto bookhive records" 5 5 requires-python = ">=3.11" 6 6 dependencies = [ 7 7 "fastapi>=0.115", ··· 24 24 build-backend = "hatchling.build" 25 25 26 26 [tool.hatch.build.targets.wheel] 27 - packages = ["src/waggle"] 27 + packages = ["src/spacebee"] 28 28 29 29 [tool.ruff] 30 30 line-length = 100
src/waggle/__init__.py src/spacebee/__init__.py
src/waggle/adapters/__init__.py src/spacebee/adapters/__init__.py
src/waggle/adapters/web/__init__.py src/spacebee/adapters/web/__init__.py
+3 -3
src/waggle/adapters/web/router.py src/spacebee/adapters/web/router.py
··· 9 9 from fastapi.responses import HTMLResponse, Response 10 10 from jinja2 import Environment, PackageLoader, select_autoescape 11 11 12 - from waggle.adapters.webdav.router import DAVContext 13 - from waggle.atproto import bookhive 12 + from spacebee.adapters.webdav.router import DAVContext 13 + from spacebee.atproto import bookhive 14 14 15 15 from . import view 16 16 17 17 log = logging.getLogger(__name__) 18 18 19 19 _env = Environment( 20 - loader=PackageLoader("waggle.adapters.web", "templates"), 20 + loader=PackageLoader("spacebee.adapters.web", "templates"), 21 21 autoescape=select_autoescape(["html"]), 22 22 trim_blocks=True, 23 23 lstrip_blocks=True,
+3 -3
src/waggle/adapters/web/templates/dashboard.html src/spacebee/adapters/web/templates/dashboard.html
··· 3 3 <head> 4 4 <meta charset="utf-8"> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1"> 6 - <title>Reading · {{ profile.handle or "waggle" }}</title> 6 + <title>Reading · {{ profile.handle or "spacebee" }}</title> 7 7 <style> 8 - /* Gruvbox palette, lifted from brad.quest */ 8 + /* Gruvbox palette */ 9 9 :root { 10 10 --bg-main: #fbf1c7; 11 11 --bg-muted: #f2e5bc; ··· 243 243 {% if profile_url %}<a href="{{ profile_url }}" target="_blank" rel="noopener" class="profile-link">{% endif %} 244 244 <img src="{{ profile.avatar or '' }}" alt=""> 245 245 <div> 246 - <p class="who">{{ profile.displayName or profile.handle or "waggle" }}</p> 246 + <p class="who">{{ profile.displayName or profile.handle or "spacebee" }}</p> 247 247 <p class="meta"> 248 248 {% if profile.handle %}@{{ profile.handle }} · {% endif %}{{ book_count }} book{{ '' if book_count == 1 else 's' }} on Bookhive 249 249 </p>
src/waggle/adapters/web/view.py src/spacebee/adapters/web/view.py
src/waggle/adapters/webdav/__init__.py src/spacebee/adapters/webdav/__init__.py
+2 -2
src/waggle/adapters/webdav/moonreader.py src/spacebee/adapters/webdav/moonreader.py
··· 13 13 from email.utils import format_datetime 14 14 from urllib.parse import quote 15 15 16 - from waggle.atproto import bookhive 17 - from waggle.atproto.client import ATProtoClient 16 + from spacebee.atproto import bookhive 17 + from spacebee.atproto.client import ATProtoClient 18 18 19 19 log = logging.getLogger(__name__) 20 20
+1 -1
src/waggle/adapters/webdav/passthrough.py src/spacebee/adapters/webdav/passthrough.py
··· 1 1 """Local-disk WebDAV fallback for paths outside the Moon+ cache virtual tree. 2 2 3 3 Moon+ Reader syncs non-position files (settings, bookmark lists, theme data) 4 - alongside `.po` files. waggle stores those on disk under `PASSTHROUGH_ROOT` and 4 + alongside `.po` files. spacebee stores those on disk under `PASSTHROUGH_ROOT` and 5 5 serves them back over DAV. Eventually, annotation-bearing files here may be 6 6 routed to `margin.at` records — out of scope for v1. 7 7 """
+1 -1
src/waggle/adapters/webdav/router.py src/spacebee/adapters/webdav/router.py
··· 14 14 15 15 from fastapi import APIRouter, Request, Response 16 16 17 - from waggle.atproto.client import ATProtoClient 17 + from spacebee.atproto.client import ATProtoClient 18 18 19 19 from . import moonreader 20 20 from .passthrough import Passthrough
src/waggle/atproto/__init__.py src/spacebee/atproto/__init__.py
+2 -7
src/waggle/atproto/bookhive.py src/spacebee/atproto/bookhive.py
··· 1 1 """Translation layer between Moon+ Reader `.po` files and buzz.bookhive.book records. 2 2 3 - The parsing / matching helpers are lifted (with light edits) from 4 - `../moon2hive/moon2hive.py`. The ATProto calls are rewritten to go through 5 - `ATProtoClient` instead of the old sync `requests` path. 6 - 7 3 Namespace contract: 8 4 9 5 bookProgress.moonReader.{position, file, syncedAt} ··· 11 7 `moonReader.position` is the raw `.po` content byte-for-byte. We preserve it on 12 8 write so GETs (which return this verbatim) continue to produce a file Moon+ 13 9 Reader recognizes — including its internal `timestamp_ms` which is the ebook 14 - file's import mtime, not a real timestamp. See the moon2hive CLAUDE.md for the 15 - full story. 10 + file's import mtime, not a real timestamp. 16 11 """ 17 12 18 13 from __future__ import annotations ··· 32 27 BOOKHIVE_COLLECTION = "buzz.bookhive.book" 33 28 BOOKHIVE_CATALOG_URL = "https://bookhive.buzz/xrpc/buzz.bookhive.searchBooks" 34 29 35 - # Records cache: single-user waggle, so a module-level dict suffices. 30 + # Records cache: single-user spacebee, so a module-level dict suffices. 36 31 _RECORDS_CACHE: dict[str, tuple[float, list[dict]]] = {} 37 32 RECORDS_TTL = 30.0 38 33
src/waggle/atproto/client.py src/spacebee/atproto/client.py
+2 -2
src/waggle/auth.py src/spacebee/auth.py
··· 1 1 """HTTP Basic gate on all DAV requests. 2 2 3 - Single shared credential — waggle only serves one ATProto identity, so the 3 + Single shared credential — spacebee only serves one ATProto identity, so the 4 4 Basic auth here just lets Moon+ Reader say "I'm allowed to talk to this box." 5 5 It does not map to different atproto accounts. 6 6 """ ··· 27 27 28 28 29 29 def challenge() -> Response: 30 - return Response(status_code=401, headers={"WWW-Authenticate": 'Basic realm="waggle"'}) 30 + return Response(status_code=401, headers={"WWW-Authenticate": 'Basic realm="spacebee"'})
+1 -1
src/waggle/config.py src/spacebee/config.py
··· 10 10 11 11 @dataclass(frozen=True) 12 12 class Config: 13 - # ATProto identity waggle writes to 13 + # ATProto identity spacebee writes to 14 14 pds: str 15 15 bsky_handle: str 16 16 bsky_app_password: str
+8 -8
src/waggle/main.py src/spacebee/main.py
··· 8 8 from fastapi import FastAPI, Request 9 9 from starlette.middleware.base import BaseHTTPMiddleware 10 10 11 - from waggle import auth, config 12 - from waggle.adapters.web import make_router as make_web_router 13 - from waggle.adapters.webdav.passthrough import Passthrough 14 - from waggle.adapters.webdav.router import DAVContext, make_router 15 - from waggle.atproto.client import ATProtoClient 11 + from spacebee import auth, config 12 + from spacebee.adapters.web import make_router as make_web_router 13 + from spacebee.adapters.webdav.passthrough import Passthrough 14 + from spacebee.adapters.webdav.router import DAVContext, make_router 15 + from spacebee.atproto.client import ATProtoClient 16 16 17 17 18 18 def create_app() -> FastAPI: ··· 22 22 format="%(asctime)s %(levelname)s %(name)s: %(message)s", 23 23 datefmt="%H:%M:%S", 24 24 ) 25 - log = logging.getLogger("waggle") 25 + log = logging.getLogger("spacebee") 26 26 27 27 client = ATProtoClient(cfg.pds, cfg.bsky_handle, cfg.bsky_app_password) 28 28 passthrough = Passthrough(cfg.passthrough_root) ··· 30 30 31 31 @asynccontextmanager 32 32 async def lifespan(_: FastAPI): 33 - log.info("waggle starting (pds=%s handle=%s)", cfg.pds, cfg.bsky_handle) 33 + log.info("spacebee starting (pds=%s handle=%s)", cfg.pds, cfg.bsky_handle) 34 34 try: 35 35 yield 36 36 finally: 37 37 await client.close() 38 - log.info("waggle stopped") 38 + log.info("spacebee stopped") 39 39 40 40 app = FastAPI(lifespan=lifespan, openapi_url=None, docs_url=None, redoc_url=None) 41 41
+1 -1
tests/test_atproto_client.py
··· 6 6 import pytest 7 7 import respx 8 8 9 - from waggle.atproto.client import ATProtoClient 9 + from spacebee.atproto.client import ATProtoClient 10 10 11 11 12 12 @pytest.fixture
+1 -1
tests/test_bookhive_parsing.py
··· 1 1 """Pure-function tests for `.po` parsing and filename heuristics.""" 2 2 3 - from waggle.atproto import bookhive 3 + from spacebee.atproto import bookhive 4 4 5 5 6 6 class TestParsePo:
+3 -3
tests/test_web_e2e.py
··· 9 9 import respx 10 10 from fastapi.testclient import TestClient 11 11 12 - from waggle.atproto import bookhive 12 + from spacebee.atproto import bookhive 13 13 14 14 15 15 @pytest.fixture 16 16 def app(monkeypatch): 17 17 bookhive.invalidate_cache() 18 - tmp = tempfile.mkdtemp(prefix="waggle-web-test-") 18 + tmp = tempfile.mkdtemp(prefix="spacebee-web-test-") 19 19 monkeypatch.setenv("PDS", "pds.example") 20 20 monkeypatch.setenv("BSKY_HANDLE", "tester.example") 21 21 monkeypatch.setenv("BSKY_APP_PASSWORD", "app-pw") 22 22 monkeypatch.setenv("DAV_USER", "u") 23 23 monkeypatch.setenv("DAV_PASSWORD", "p") 24 24 monkeypatch.setenv("PASSTHROUGH_ROOT", tmp) 25 - from waggle import main as main_mod 25 + from spacebee import main as main_mod 26 26 return main_mod.create_app() 27 27 28 28
+1 -1
tests/test_web_view.py
··· 2 2 3 3 from __future__ import annotations 4 4 5 - from waggle.adapters.web.view import build_books_view, cover_cids, partition 5 + from spacebee.adapters.web.view import build_books_view, cover_cids, partition 6 6 7 7 8 8 def _rec(title: str, **overrides) -> dict:
+3 -3
tests/test_webdav_e2e.py
··· 9 9 import respx 10 10 from fastapi.testclient import TestClient 11 11 12 - from waggle.atproto import bookhive 12 + from spacebee.atproto import bookhive 13 13 14 14 15 15 @pytest.fixture ··· 17 17 # Clear module-level record cache between tests. 18 18 bookhive.invalidate_cache() 19 19 20 - tmp = tempfile.mkdtemp(prefix="waggle-test-") 20 + tmp = tempfile.mkdtemp(prefix="spacebee-test-") 21 21 monkeypatch.setenv("PDS", "pds.example") 22 22 monkeypatch.setenv("BSKY_HANDLE", "tester.example") 23 23 monkeypatch.setenv("BSKY_APP_PASSWORD", "app-pw") ··· 26 26 monkeypatch.setenv("PASSTHROUGH_ROOT", tmp) 27 27 28 28 # Reload the app factory to pick up env. 29 - from waggle import main as main_mod 29 + from spacebee import main as main_mod 30 30 31 31 return main_mod.create_app() 32 32
+34 -34
uv.lock
··· 531 531 ] 532 532 533 533 [[package]] 534 + name = "spacebee" 535 + version = "0.1.0" 536 + source = { editable = "." } 537 + dependencies = [ 538 + { name = "fastapi" }, 539 + { name = "httpx" }, 540 + { name = "jinja2" }, 541 + { name = "python-dotenv" }, 542 + { name = "uvicorn", extra = ["standard"] }, 543 + ] 544 + 545 + [package.optional-dependencies] 546 + dev = [ 547 + { name = "pytest" }, 548 + { name = "pytest-asyncio" }, 549 + { name = "respx" }, 550 + { name = "ruff" }, 551 + ] 552 + 553 + [package.metadata] 554 + requires-dist = [ 555 + { name = "fastapi", specifier = ">=0.115" }, 556 + { name = "httpx", specifier = ">=0.27" }, 557 + { name = "jinja2", specifier = ">=3.1" }, 558 + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, 559 + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23" }, 560 + { name = "python-dotenv", specifier = ">=1.0" }, 561 + { name = "respx", marker = "extra == 'dev'", specifier = ">=0.21" }, 562 + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6" }, 563 + { name = "uvicorn", extras = ["standard"], specifier = ">=0.30" }, 564 + ] 565 + provides-extras = ["dev"] 566 + 567 + [[package]] 534 568 name = "starlette" 535 569 version = "1.0.0" 536 570 source = { registry = "https://pypi.org/simple" } ··· 625 659 { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" }, 626 660 { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" }, 627 661 ] 628 - 629 - [[package]] 630 - name = "waggle" 631 - version = "0.1.0" 632 - source = { editable = "." } 633 - dependencies = [ 634 - { name = "fastapi" }, 635 - { name = "httpx" }, 636 - { name = "jinja2" }, 637 - { name = "python-dotenv" }, 638 - { name = "uvicorn", extra = ["standard"] }, 639 - ] 640 - 641 - [package.optional-dependencies] 642 - dev = [ 643 - { name = "pytest" }, 644 - { name = "pytest-asyncio" }, 645 - { name = "respx" }, 646 - { name = "ruff" }, 647 - ] 648 - 649 - [package.metadata] 650 - requires-dist = [ 651 - { name = "fastapi", specifier = ">=0.115" }, 652 - { name = "httpx", specifier = ">=0.27" }, 653 - { name = "jinja2", specifier = ">=3.1" }, 654 - { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0" }, 655 - { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23" }, 656 - { name = "python-dotenv", specifier = ">=1.0" }, 657 - { name = "respx", marker = "extra == 'dev'", specifier = ">=0.21" }, 658 - { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.6" }, 659 - { name = "uvicorn", extras = ["standard"], specifier = ">=0.30" }, 660 - ] 661 - provides-extras = ["dev"] 662 662 663 663 [[package]] 664 664 name = "watchfiles"