# pypx — CLAUDE.md ## What this project is pypx is a modern frontend for PyPI (Python Package Index). It enriches PyPI's public data with search, dependency analysis, download stats, changelogs, security advisories, and API docs. Live at **https://pypx.app**. ## Stack | Layer | Technology | |---|---| | API | Go 1.27, chi v5, goldmark (markdown→HTML), modernc.org/sqlite | | Frontend | Nuxt 4, Vue 3, Tailwind 4, VueUse, @nuxtjs/seo | | Search | SQLite FTS5 (porter + unicode61 tokenizer) | | Cache | Two-tier: in-memory LRU (1,000 entries) + SQLite persistent TTL store | | Doc extraction | Go, goopy (pure Go in-process wheel parser) | | Proxy | Caddy 2 | | Deploy | Docker Compose (Caddy + Go API + Nuxt) | ## Directory Map ``` api/ Go API service cmd/server/main.go Entry point — router, service wiring, graceful shutdown internal/ cache/ Two-tier cache (memory.go, sqlite.go) handler/ HTTP handlers (one file per route group) pypi/ PyPI JSON API client stats/ pypistats.org client github/ GitHub API client + source implementations gitlab/ GitLab API client + source implementations osv/ OSV (vulnerability) client conda/ conda-forge availability client enrichment/ Pure functions: dependencies, wheels, platforms, cadence, maintainers changelog/ Parallel source registry + parser + renderer search/ FTS5 index (index.go) worker/ Background sync worker (background.go) textfmt/ Plain-text formatters for agent .txt endpoints (one file per resource) web/ Nuxt 4 frontend app/ pages/ Route components (index.vue, popular.vue, compare.vue, packages/[name].vue) components/ Reusable UI components composables/ useApi.ts, useSearchTypeahead.ts, usePackageManager.ts types/api.ts TypeScript interfaces matching Go response shapes nuxt.config.ts Build config, runtime config, SEO config goopy/ Pure Go Python doc extractor (in-process) internal/ Parser, extractor, wheel downloader, ecosystem tests docs/architecture/ Architecture documentation (Mermaid diagrams) Caddyfile Reverse proxy routing + security headers docker-compose.yml Full stack definition ``` ## Key Files | File | Purpose | |---|---| | `api/cmd/server/main.go` | Service wiring, router setup, env var defaults | | `api/internal/cache/sqlite.go` | Persistent TTL cache with stale-while-revalidate | | `api/internal/cache/memory.go` | LRU memory layer wrapping SQLite cache | | `api/internal/handler/packages.go` | Core package endpoint — enrichment orchestration | | `api/internal/handler/changelog.go` | Changelog endpoint — delegates to registry | | `api/internal/handler/llms.go` | Discovery endpoint for plain-text routes | | `api/internal/textfmt/` | Pure formatter functions emitting plain text for /api/.../*.txt routes | | `api/internal/changelog/registry.go` | Parallel source fetch with priority selection | | `api/internal/search/index.go` | FTS5 index: Search(), TopByDownloads(), UpsertBatch() | | `api/internal/worker/background.go` | Index sync: SyncIndex() + SyncDownloads(), runs every 6h | | `api/internal/enrichment/dependencies.go` | PEP 508 `requires_dist` parser | | `api/internal/enrichment/wheels.go` | Install size + platform coverage from distribution files | | `web/app/composables/useApi.ts` | API wrapper — handles server vs. client base URL | | `web/app/pages/packages/[name].vue` | Main package page — SSR + client-side parallel loads | | `web/app/types/api.ts` | TypeScript type shim — re-exports from `api.gen.ts`; run `cd api && go run ./cmd/gentypes -out ../web/app/types/api.gen.ts` after changing a response struct | | `web/app/types/api.gen.ts` | Generated TypeScript interfaces (DO NOT EDIT — commit after regenerating) | | `api/cmd/gentypes/main.go` | Generator — reflects on Go response structs to emit `api.gen.ts` | | `web/nuxt.config.ts` | Runtime config: `apiBase` (server), `public.apiBase` (client); `fonts` (self-hosted Geist) | | `scripts/check-fonts-bundled.mjs` | Post-build guard: brand fonts must ship self-hosted, never from a third-party host. Run via `make check-fonts` after a web build; CI runs it in `web-build`. | ## Development Commands The root `Makefile` is the source of truth for dev/test/lint/gentypes commands — run `make help` for the full list. ```bash make dev-api # Run Go API on :8080 make dev-web # Run Nuxt dev server on :3000 (proxies /api/* to :8080) make test # Run all test suites (api, goopy, web) make test-api # Go API tests make test-web # Web tests (vitest) make test-goopy # goopy tests (-short skips the scheduled ecosystem suite) make lint # go vet (api, goopy) + oxlint (web) make typecheck # Web typecheck (nuxi typecheck) make gentypes # Regenerate web/app/types/api.gen.ts from Go response structs # Full stack docker compose up --build # Production-like stack ``` ## Patterns to Follow ### Cache keys ``` raw:{name} Raw PyPI JSON response pkg:{name} Enriched package response stats:{name}:{period} Download stats (period: 4w, 3m, 6m) changelog:{name} Rendered changelog HTML docs:v{ExtractorVersion}:{name}:{version} API docs (indefinite TTL; bump goopy.ExtractorVersion when extraction output changes) docs-err:v{ExtractorVersion}:{name}:{version} Docs extraction error (5 min TTL) ``` ### Handler structure Every handler follows: check cache → fetch live data → enrich → cache → return JSON. For stale-while-revalidate: return stale immediately, trigger background goroutine to refresh. ### Changelog source interface ```go type Source interface { Fetch(ctx context.Context) ([]Entry, error) Priority() int } ``` Registry fires all sources concurrently, picks first non-empty result by ascending priority number. ### Enrichment functions All enrichment is pure functions in `api/internal/enrichment/`. Input: raw PyPI data. Output: computed structs. No side effects, no I/O. ### Frontend data loading - **SSR (blocking):** Package metadata (`/api/packages/{name}`) — fetched server-side via `useAsyncData` - **Client-side (parallel, non-blocking):** Changelog, security, extras, docs — loaded after hydration with `server: false` ## SQL Gotcha Always use `?` placeholders, not `$1`/`$2`. modernc.org/sqlite uses `?` binding syntax. ```go // Correct db.QueryRow("SELECT value FROM cache WHERE key = ?", key) // Wrong — will break db.QueryRow("SELECT value FROM cache WHERE key = $1", key) ``` ## Cache TTLs | Endpoint | TTL | Notes | |---|---|---| | Package metadata | 1 hour | Stale-while-revalidate | | Stats | 24 hours | Stale-while-revalidate | | Changelog | 7 days | Fresh only | | Security | 24 hours | Stale-while-revalidate | | Extras | 24 hours | Stale-while-revalidate | | Docs | Indefinite | Keyed by version — never stale | | Docs errors | 5 minutes | Short TTL to allow retry | | Search | 5 min (HTTP header) | Not in SQLite cache | ## GitHub/GitLab Tokens Both are optional. Without `GITHUB_TOKEN`, GitHub API is rate-limited to 60 req/hr (per IP). With token: 5,000 req/hr. Set in `.env` for local dev or as Docker env vars in production. GitLab token similarly optional — only needed for packages hosted on GitLab. ## goopy (Python Doc Extraction) The Go API extracts Python API documentation in-process using the `goopy` library (`goopy/` directory). goopy downloads wheels from PyPI, parses Python source with a recursive-descent parser, and extracts structured documentation (functions, classes, parameters, docstrings, type annotations). The `/api/packages/{name}/docs` route has a **60-second timeout** (vs. 30s for all other routes) to accommodate wheel download + goopy parse time. ## External Services | Service | Used for | Auth | |---|---|---| | `pypi.org/pypi/{name}/json` | Package metadata | None | | `pypi.org/simple/?format=application/vnd.pypi.simple.v1+json` | All package names (780K+) | None | | `pypistats.org/api/packages/{name}/...` | Download stats | None | | `api.github.com` | Releases, tags, raw files, repo info | Optional token | | `gitlab.com/api/v4` | GitLab releases, files | Optional token | | `api.osv.dev` | CVE/vulnerability data | None | | `conda.anaconda.org` | conda-forge availability | None | | `hugovk.dev/top-pypi-packages/...` | Top 30-day download counts | None |