From e004043fd726b4da7f1666d4d6a421d4be8e511c Mon Sep 17 00:00:00 2001 From: prosdev Date: Mon, 19 Jan 2026 21:19:23 -0800 Subject: [PATCH] docs: add Nextra site with GitHub Pages deployment Co-Authored-By: Claude Sonnet 4.5 --- .github/workflows/docs.yml | 65 + README.md | 2 + docs/.gitignore | 36 + docs/README.md | 54 + docs/app/[[...mdxPath]]/page.tsx | 23 + docs/app/layout.tsx | 55 + docs/content/advanced/index.mdx | 168 + docs/content/api/index.mdx | 267 ++ docs/content/contributing/index.mdx | 537 +++ docs/content/contributing/spec-process.mdx | 130 + docs/content/contributing/testing.mdx | 475 ++ docs/content/core-concepts/architecture.mdx | 488 ++ docs/content/core-concepts/index.mdx | 172 + docs/content/getting-started/index.mdx | 100 + docs/content/guides/index.mdx | 125 + docs/content/guides/local-development.mdx | 284 ++ docs/content/index.mdx | 63 + docs/content/specifications/index.mdx | 125 + docs/mdx-components.tsx | 3 + docs/next.config.mjs | 24 + docs/package.json | 24 + docs/pnpm-lock.yaml | 4393 +++++++++++++++++++ docs/public/favicon.ico | 1 + 23 files changed, 7614 insertions(+) create mode 100644 .github/workflows/docs.yml create mode 100644 docs/.gitignore create mode 100644 docs/README.md create mode 100644 docs/app/[[...mdxPath]]/page.tsx create mode 100644 docs/app/layout.tsx create mode 100644 docs/content/advanced/index.mdx create mode 100644 docs/content/api/index.mdx create mode 100644 docs/content/contributing/index.mdx create mode 100644 docs/content/contributing/spec-process.mdx create mode 100644 docs/content/contributing/testing.mdx create mode 100644 docs/content/core-concepts/architecture.mdx create mode 100644 docs/content/core-concepts/index.mdx create mode 100644 docs/content/getting-started/index.mdx create mode 100644 docs/content/guides/index.mdx create mode 100644 docs/content/guides/local-development.mdx create mode 100644 docs/content/index.mdx create mode 100644 docs/content/specifications/index.mdx create mode 100644 docs/mdx-components.tsx create mode 100644 docs/next.config.mjs create mode 100644 docs/package.json create mode 100644 docs/pnpm-lock.yaml create mode 100644 docs/public/favicon.ico diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..aa43844 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,65 @@ +name: Deploy Docs + +on: + push: + branches: [main] + paths: ['docs/**', '.github/workflows/docs.yml'] + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9.15.4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'pnpm' + cache-dependency-path: 'docs/pnpm-lock.yaml' + + - name: Install dependencies + working-directory: docs + run: pnpm install --frozen-lockfile + + - name: Build + working-directory: docs + env: + BASE_PATH: /eventkit + NODE_ENV: production + run: pnpm build + + - name: Setup Pages + uses: actions/configure-pages@v5 + + - name: Upload artifact + uses: actions/upload-pages-artifact@v4 + with: + path: './docs/out' + + deploy: + runs-on: ubuntu-latest + needs: build + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index 9efc9b6..d2f9d10 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,8 @@ Event ingestion and processing kit for Python. **Philosophy**: Provide a solid starting point with battle-tested patterns, then get out of your way. Customize for your specific needs. +**📚 [View Full Documentation](https://prosdevlab.github.io/eventkit/)** + ### Key Features - **Flexible ingestion** - Accept any JSON payload with Segment-compatible API diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..d1b55a9 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,36 @@ +# Dependencies +node_modules/ +.pnp +.pnp.js + +# Testing +coverage/ + +# Next.js +.next/ +out/ +dist/ + +# Production +build/ + +# Misc +.DS_Store +*.pem + +# Debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Local env files +.env*.local +.env + +# Vercel +.vercel + +# TypeScript +*.tsbuildinfo +next-env.d.ts diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..feec972 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,54 @@ +# eventkit Documentation Site + +This directory contains the Nextra-based documentation site for eventkit. + +## Development + +```bash +# Install dependencies +pnpm install + +# Start dev server +pnpm dev + +# Build for production +pnpm build +``` + +The dev server will start at http://localhost:3000 + +## Technology Stack + +- **Nextra v4.0.0** - Documentation framework +- **Next.js 15.1.6** - React framework +- **React 19** - UI library +- **pnpm** - Package manager + +## Project Structure + +``` +docs/ +├── app/ # Next.js app directory +│ ├── layout.tsx # Root layout with navigation +│ └── [[...mdxPath]]/ # Dynamic MDX page routing +├── content/ # MDX documentation files +│ ├── _meta.json # Navigation structure +│ └── *.mdx # Documentation pages +├── public/ # Static assets +├── next.config.mjs # Next.js configuration +├── tsconfig.json # TypeScript configuration +└── package.json # Dependencies and scripts +``` + +## Deployment + +The site is automatically deployed to GitHub Pages at https://prosdevlab.github.io/eventkit/ when changes are pushed to the `main` branch. + +## Adding Content + +1. Create an MDX file in `content/` +2. Add navigation entry to `content/_meta.json` +3. Write content using MDX syntax +4. Test locally with `pnpm dev` + +See [Nextra documentation](https://nextra.site/) for more details on MDX features and components. diff --git a/docs/app/[[...mdxPath]]/page.tsx b/docs/app/[[...mdxPath]]/page.tsx new file mode 100644 index 0000000..ae2c92f --- /dev/null +++ b/docs/app/[[...mdxPath]]/page.tsx @@ -0,0 +1,23 @@ +import { generateStaticParamsFor, importPage } from 'nextra/pages'; +import { useMDXComponents } from 'nextra-theme-docs'; + +export const generateStaticParams = generateStaticParamsFor('mdxPath'); + +export async function generateMetadata(props: { params: Promise<{ mdxPath?: string[] }> }) { + const params = await props.params; + const { metadata } = await importPage(params.mdxPath); + return metadata; +} + +export default async function Page(props: { params: Promise<{ mdxPath?: string[] }> }) { + const params = await props.params; + const result = await importPage(params.mdxPath); + const { default: MDXContent, ...rest } = result; + const { wrapper: Wrapper } = useMDXComponents(); + + return ( + + + + ); +} diff --git a/docs/app/layout.tsx b/docs/app/layout.tsx new file mode 100644 index 0000000..50b47cb --- /dev/null +++ b/docs/app/layout.tsx @@ -0,0 +1,55 @@ +import { Banner, Head } from 'nextra/components'; +import { getPageMap } from 'nextra/page-map'; +import { Footer, Layout, Navbar } from 'nextra-theme-docs'; +import 'nextra-theme-docs/style.css'; + +export const metadata = { + title: 'eventkit - Event Ingestion and Processing Kit for Python', + description: + 'A production-ready kit for building event collection pipelines with flexible ingestion, stream-based routing, and pluggable storage', +}; + +const banner = ( + + eventkit is in active development. APIs may change. + +); + +const navbar = ( + eventkit} + projectLink="https://github.com/prosdevlab/eventkit" + chatLink="https://github.com/prosdevlab/eventkit/discussions" + /> +); + +const footer = ( + +); + +export default async function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + {children} + + + + ); +} diff --git a/docs/content/advanced/index.mdx b/docs/content/advanced/index.mdx new file mode 100644 index 0000000..f0e2b78 --- /dev/null +++ b/docs/content/advanced/index.mdx @@ -0,0 +1,168 @@ +--- +title: Advanced +description: Advanced patterns and production deployment guides +--- + +# Advanced + +Advanced patterns, production deployment, and performance tuning for eventkit. + +## Topics + +### Architecture Patterns + +- **Dual-Path Architecture** - Process events through multiple coordinators (coming soon) +- **Performance Tuning** - Optimize for throughput and latency (coming soon) +- **Monitoring & Observability** - Prometheus metrics and logging (coming soon) + +### Production Deployment + +- **Cloud Run** - Serverless deployment (coming soon) +- **GKE/Kubernetes** - Container orchestration (coming soon) +- **Horizontal Scaling** - Multi-instance patterns (coming soon) + +### Performance + +eventkit is designed for high throughput with low latency: + +**Throughput:** +- 10,000+ events/second validated (single instance) +- Horizontal scaling with PubSubQueue mode +- Adaptive batching reduces storage writes + +**Latency:** +- p50: less than 25ms (collection endpoint) +- p95: less than 100ms (target) +- Ring buffer adds less than 1ms overhead + +**Benchmarks:** +```bash +pytest tests/performance/ --benchmark-only +``` + +See `specs/performance-benchmarks/` for detailed results. + +### Ring Buffer (Write-Ahead Log) + +eventkit uses a ring buffer for durability - events are never lost even if the service crashes. + +**Architecture:** +```mermaid +%%{init: {'theme':'neutral', 'themeVariables': {'lineColor':'#888','edgeLabelBackground':'transparent'}}}%% +graph LR + API[API] --> RB[(Ring Buffer
SQLite WAL)] + RB --> Pub[Publisher] + Pub --> Queue[Queue] + Queue --> Workers[Workers] + Workers --> Store[(Storage)] + + classDef storage stroke-width:3px + class RB,Store storage +``` + +**Why SQLite?** +- Local durability (no network on hot path) +- WAL mode for concurrent reads/writes +- Zero dependencies +- Production-proven pattern + +**Configuration:** +```bash +export EVENTKIT_RING_BUFFER_DB_PATH="./eventkit_ring_buffer.db" +export EVENTKIT_RING_BUFFER_MAX_SIZE="100000" +export EVENTKIT_RING_BUFFER_RETENTION_HOURS="24" +``` + +### Monitoring + +eventkit exposes Prometheus metrics on port 9090: + +```bash +curl http://localhost:9090/metrics +``` + +**Key Metrics:** +- Event throughput (events/sec) +- Queue depth and processing lag +- Storage write latency +- Error rates + +**Grafana Dashboards:** +```promql +# Event processing rate +rate(eventkit_events_processed_total[5m]) + +# Queue backlog +eventkit_queue_depth + +# Error rate +rate(eventkit_events_failed_total[5m]) / rate(eventkit_events_received_total[5m]) +``` + +### Custom Adapters + +Implement custom validation and transformation logic: + +```python +from eventkit.adapters.base import SchemaAdapter, AdapterResult +from eventkit.schema.raw import RawEvent +from eventkit.schema.events import TypedEvent + +class MyCustomAdapter(SchemaAdapter): + def adapt(self, raw: RawEvent) -> AdapterResult[TypedEvent]: + # Custom validation + if not self._is_valid(raw.payload): + return AdapterResult.failure("Invalid format") + + # Custom transformation + event = self._transform(raw.payload) + return AdapterResult.success(event) + + def _is_valid(self, payload: dict) -> bool: + # Your validation logic + return True + + def _transform(self, payload: dict) -> TypedEvent: + # Your transformation logic + return TypedEvent(...) +``` + +### Error Handling + +eventkit never drops events - validation failures go to the error store: + +```python +# Invalid events are stored for debugging +errors = await error_store.query_errors(limit=100) +for error in errors: + print(f"Error: {error['error']}") + print(f"Payload: {error['payload']}") + print(f"Timestamp: {error['timestamp']}") +``` + +**Common Error Patterns:** +- Missing required fields +- Type mismatches +- Invalid timestamps +- Unknown event types + +### Security + +**Authentication & Authorization:** +eventkit doesn't provide built-in auth. Use a reverse proxy (Cloud Run, API Gateway) for authentication. + +**PII Handling:** +- Never log full event payloads +- Use structured logging with allowlists +- Consider field-level encryption for sensitive data + +**Network Security:** +- Run behind load balancer (TLS termination) +- Use VPC for internal services +- Enable Cloud Armor for DDoS protection + +## Next Steps + +- **[Architecture](/core-concepts/architecture)** - Deep dive into system design +- **[Configuration](/api/configuration)** - Environment variables reference +- **[Contributing](/contributing)** - Contribute to eventkit diff --git a/docs/content/api/index.mdx b/docs/content/api/index.mdx new file mode 100644 index 0000000..fd13ea0 --- /dev/null +++ b/docs/content/api/index.mdx @@ -0,0 +1,267 @@ +--- +title: API Reference +description: Complete API reference for eventkit +--- + +# API Reference + +Complete reference for eventkit's HTTP API, configuration, and protocols. + +## HTTP API + +### Health Endpoints + +```bash +GET /health +``` + +**Liveness probe** - Returns 200 if process is running (no dependencies checked). + +```json +{"status": "ok"} +``` + +```bash +GET /ready +``` + +**Readiness probe** - Returns 200 if dependencies (Firestore, GCS) are healthy, 503 if not. + +```json +{"status": "ready"} +``` + +### Collection Endpoints + +```bash +POST /collect/{stream} +POST /collect +``` + +**Flexible event collection** - Accept any JSON payload. + +**Parameters:** +- `stream` (path, optional): Stream name for routing (default: "default") + +**Request Body:** +- Single event (object) +- Batch of events (array) + +**Response:** 202 Accepted +```json +{"status": "accepted"} +``` + +**Example:** +```bash +curl -X POST http://localhost:8000/collect/users \ + -H "Content-Type: application/json" \ + -d '{"type": "identify", "userId": "user_123", "traits": {"email": "user@example.com"}}' +``` + +### Segment-Compatible Endpoints + +```bash +POST /v1/identify +``` + +**Identify users** - Create or update user profiles. + +**Request Body:** +```json +{ + "type": "identify", + "userId": "user_123", + "traits": { + "email": "user@example.com", + "name": "John Doe", + "plan": "enterprise" + } +} +``` + +Routes to `users` stream. + +```bash +POST /v1/track +``` + +**Track events** - Record user actions. + +**Request Body:** +```json +{ + "type": "track", + "userId": "user_123", + "event": "Button Clicked", + "properties": { + "button_id": "cta-signup", + "page": "/home" + } +} +``` + +Routes to `events` stream. + +```bash +POST /v1/page +``` + +**Track page views** - Record page navigation. + +**Request Body:** +```json +{ + "type": "page", + "userId": "user_123", + "name": "Home Page", + "properties": { + "url": "/home", + "referrer": "/landing" + } +} +``` + +Routes to `pages` stream. + +## Configuration + +See **[Configuration Reference](/api/configuration)** for all environment variables. + +**Key Settings:** + +| Category | Variable | Default | Description | +|----------|----------|---------|-------------| +| **Storage** | `EVENTKIT_EVENT_STORE` | `"gcs"` | Storage backend (`gcs`, `firestore`) | +| | `GCP_GCS_BUCKET` | *required* | GCS bucket name | +| | `GCP_BIGQUERY_DATASET` | *required* | BigQuery dataset | +| **Queue** | `EVENTKIT_QUEUE_MODE` | `"async"` | Queue mode (`async`, `pubsub`) | +| | `EVENTKIT_ASYNC_WORKERS` | `4` | Number of async workers | +| **Ring Buffer** | `EVENTKIT_RING_BUFFER_DB_PATH` | `"eventkit_ring_buffer.db"` | SQLite database path | +| | `EVENTKIT_RING_BUFFER_MAX_SIZE` | `100000` | Max events to keep | + +## Protocols + +eventkit defines several protocols (interfaces) for extensibility. + +### EventStore Protocol + +```python +class EventStore(Protocol): + async def store(self, event: TypedEvent) -> None: + """Store a single event.""" + + async def store_batch(self, events: list[TypedEvent]) -> None: + """Store a batch of events (more efficient).""" + + def health_check(self) -> bool: + """Check if storage backend is healthy.""" +``` + +**Implementations:** +- `GCSEventStore` - Google Cloud Storage (default) +- `FirestoreEventStore` - Firestore (development/testing) + +**Custom Implementation:** +```python +class MyCustomStore(EventStore): + async def store_batch(self, events: list[TypedEvent]) -> None: + # Your implementation + pass + + def health_check(self) -> bool: + # Your implementation + return True +``` + +### EventQueue Protocol + +```python +class EventQueue(Protocol): + async def enqueue(self, event: RawEvent) -> None: + """Add event to queue.""" + + async def start(self) -> None: + """Start queue processing.""" + + async def stop(self) -> None: + """Stop queue processing gracefully.""" +``` + +**Implementations:** +- `AsyncQueue` - In-process async workers (default) +- `PubSubQueue` - Google Pub/Sub distributed workers + +### WarehouseLoader Protocol + +```python +class WarehouseLoader(Protocol): + async def start(self) -> None: + """Start background loading process.""" + + async def stop(self) -> None: + """Stop background loading process.""" + + async def load_files(self, file_paths: list[str]) -> None: + """Load specific files (for manual triggering).""" +``` + +**Implementations:** +- `BigQueryLoader` - GCS → BigQuery (default) +- **Future:** `SnowflakeLoader`, `RedshiftLoader`, `ClickHouseLoader` + +## Event Schema + +### RawEvent + +Flexible container for any JSON payload. + +```python +class RawEvent(BaseModel): + payload: dict[str, Any] # Original JSON + received_at: datetime # Ingestion timestamp + stream: str | None = None # Routing/isolation +``` + +### TypedEvent + +Base class for validated events. + +```python +class TypedEvent(BaseModel): + event_id: str + timestamp: datetime + user_id: str | None + anonymous_id: str | None + event_type: str + properties: dict[str, Any] + stream: str | None +``` + +**Subclasses:** +- `IdentifyEvent` - User identification +- `TrackEvent` - User actions +- `PageEvent` - Page views + +## Metrics + +Prometheus metrics exposed on port 9090 (configurable). + +```bash +curl http://localhost:9090/metrics +``` + +**Key Metrics:** +- `eventkit_events_received_total` - Events received +- `eventkit_events_processed_total` - Events processed +- `eventkit_events_failed_total` - Events failed validation +- `eventkit_queue_depth` - Current queue depth +- `eventkit_storage_bytes_written_total` - Bytes written + +See **[Monitoring](/advanced/monitoring)** for complete metrics reference. + +## Next Steps + +- **[Configuration Reference](/api/configuration)** - Complete environment variable reference +- **[Protocols](/api/protocols)** - Implement custom backends +- **[Local Development](/guides/local-development)** - Try the API locally diff --git a/docs/content/contributing/index.mdx b/docs/content/contributing/index.mdx new file mode 100644 index 0000000..a8022f6 --- /dev/null +++ b/docs/content/contributing/index.mdx @@ -0,0 +1,537 @@ +--- +title: Contributing +description: Guide to contributing to eventkit +--- + +# Contributing to eventkit + +Thank you for your interest in contributing to **eventkit**! This guide will help you get started. + +## Code of Conduct + +This project follows the [Contributor Covenant Code of Conduct](https://www.contributor-covenant.org/version/2/1/code_of_conduct/). By participating, you are expected to uphold this code. + +## Getting Started + +### Prerequisites + +- Python 3.11 or 3.12 +- Git +- Google Cloud SDK (for Firestore emulator) + +### Fork and Clone + +1. Fork the repository on GitHub +2. Clone your fork: + +```bash +git clone https://github.com/YOUR_USERNAME/eventkit.git +cd eventkit +``` + +3. Add upstream remote: + +```bash +git remote add upstream https://github.com/prosdev/eventkit.git +``` + +### Install Dependencies + +```bash +# Install in editable mode with dev dependencies +pip install -e ".[dev]" + +# Set up pre-commit hooks (optional but recommended) +pre-commit install +``` + +### Start Firestore Emulator (for integration tests) + +```bash +gcloud emulators firestore start --host-port=localhost:8080 +``` + +In another terminal: +```bash +export FIRESTORE_EMULATOR_HOST=localhost:8080 +``` + +### Verify Setup + +```bash +# Run tests +pytest + +# Type check +mypy src/eventkit + +# Lint +ruff check src/ +``` + +## Development Workflow + +We follow **Spec-Driven Development** (inspired by [GitHub's spec-kit](https://github.com/github/spec-kit)): + +1. **Spec** → Define what to build (`specs/*/spec.md`) +2. **Plan** → Define how to build it (`specs/*/plan.md`) +3. **Tasks** → Break into atomic tasks (`specs/*/tasks.md`) +4. **Implement** → Build it, following TDD + +See [Spec-Driven Development](/contributing/spec-process) for detailed process. + +### Working on a Feature + +1. **Check existing specs** in `specs/` directory +2. **Pick a task** from `specs/*/tasks.md` +3. **Create a branch**: + +```bash +git checkout -b feature/P1-T001-raw-event-model +``` + +4. **Write tests first** (TDD): + +```bash +# Create test file +touch tests/unit/schema/test_raw.py + +# Write failing tests +# Then implement to make them pass +``` + +5. **Implement the feature** +6. **Run tests**: + +```bash +pytest tests/unit/schema/test_raw.py -v +``` + +7. **Check quality**: + +```bash +# Type check +mypy src/eventkit + +# Lint +ruff check src/ + +# Format +ruff format src/ +``` + +8. **Commit with conventional commit message** (see below) +9. **Push and create PR** + +## Project Structure + +``` +eventkit/ +├── src/ +│ └── eventkit/ # Source code +│ ├── schema/ # Data models (RawEvent, TypedEvent) +│ ├── adapters/ # Event adapters and validators +│ ├── processing/ # Sequencer, buffer, processor +│ ├── stores/ # Storage interfaces and implementations +│ ├── api/ # FastAPI routes +│ ├── logging/ # Structured logging +│ └── errors/ # Custom exceptions +├── tests/ +│ ├── unit/ # Fast, isolated tests +│ ├── integration/ # Multi-component tests +│ └── performance/ # Throughput and latency tests +├── specs/ # Specifications and plans +│ └── core-pipeline/ +│ ├── spec.md # User stories +│ ├── plan.md # Implementation plan +│ └── tasks.md # Task breakdown +├── examples/ # Usage examples +├── CLAUDE.md # AI agent context +├── WORKFLOW.md # Spec-driven workflow +├── TESTING.md # Testing guide +└── CONTRIBUTING.md # This file +``` + +## Coding Standards + +### Python Style + +- **PEP 8** compliant (enforced by `ruff`) +- **Type hints required** (enforced by `mypy` in strict mode) +- **Docstrings** for public APIs (Google style) +- **Maximum line length**: 100 characters + +### Code Patterns + +#### 1. Use Protocols over Abstract Base Classes + +✅ **Good**: +```python +from typing import Protocol + +class EventStore(Protocol): + async def write(self, events: list[TypedEvent]) -> None: ... +``` + +❌ **Avoid**: +```python +from abc import ABC, abstractmethod + +class EventStore(ABC): + @abstractmethod + async def write(self, events: list[TypedEvent]) -> None: ... +``` + +#### 2. Use Pydantic v2 Patterns + +✅ **Good**: +```python +from pydantic import BaseModel, ConfigDict, Field + +class RawEvent(BaseModel): + model_config = ConfigDict(extra="allow") + payload: dict[str, Any] +``` + +❌ **Avoid**: +```python +class RawEvent(BaseModel): + class Config: + extra = "allow" # Pydantic v1 syntax +``` + +#### 3. Never Reject at Edge + +✅ **Good**: +```python +def adapt(self, raw: RawEvent) -> AdapterResult: + if not self._is_valid(raw): + return AdapterResult.err("Invalid event") # Return error + return AdapterResult.ok(event) +``` + +❌ **Avoid**: +```python +def adapt(self, raw: RawEvent) -> TypedEvent: + if not self._is_valid(raw): + raise ValidationError("Invalid event") # Never raise in hot path +``` + +#### 4. Async Throughout + +✅ **Good**: +```python +async def enqueue(self, event: RawEvent) -> None: + await self.processor.enqueue(event) +``` + +❌ **Avoid**: +```python +def enqueue(self, event: RawEvent) -> None: + # Blocking I/O + self.store.write(event) +``` + +#### 5. Explicit is Better than Implicit + +✅ **Good**: +```python +def get_partition_id(self, event: TypedEvent) -> int: + """Route event to partition by identity hash. + + Uses FNV-1a hash for good distribution. Events with same userId + always route to same partition for ordered processing. + """ + routing_key = self._get_routing_key(event) + hash_value = self._fnv1a_hash(routing_key) + return hash_value % self.num_partitions +``` + +❌ **Avoid**: +```python +def get_partition_id(self, event: TypedEvent) -> int: + # Hash and modulo + return hash(event.userId or event.anonymousId) % self.num_partitions +``` + +## Testing Guidelines + +See [Testing Guidelines](/contributing/testing) for comprehensive testing guide. + +### Quick Reference + +- **Unit tests**: Fast, isolated, mock dependencies +- **Integration tests**: Multi-component, use Firestore emulator +- **Performance tests**: Validate throughput/latency targets +- **Coverage target**: >80% + +### Test-Driven Development (TDD) + +**Always write tests before implementation**: + +```bash +# 1. Write failing test +# tests/unit/schema/test_raw.py +def test_raw_event_accepts_arbitrary_fields(): + event = RawEvent(payload={"custom": "field"}, stream="test") + assert event.get("custom") == "field" + +# 2. Run test (should fail) +pytest tests/unit/schema/test_raw.py::test_raw_event_accepts_arbitrary_fields + +# 3. Implement feature +# src/eventkit/schema/raw.py +class RawEvent(BaseModel): + # ... implementation + +# 4. Run test (should pass) +pytest tests/unit/schema/test_raw.py::test_raw_event_accepts_arbitrary_fields +``` + +## Commit Message Guidelines + +We use [Conventional Commits](https://www.conventionalcommits.org/) enforced by `gitlint`. + +### Format + +``` +(): + + + +