# Coding guidelines

This file provides guidance to programming agents when working with code in this repository.

## Development Commands

All commands use `uv` (package manager) and `poe` (task runner):

```bash
# Install all dependencies (dev + extras + pre-commit + playwright)
uv run poe install-dev

# Run full check suite (lint + type-check + unit tests)
uv run poe check-code

# Linting (ruff format check + ruff check)
uv run poe lint

# Auto-fix formatting
uv run poe format

# Type checking (ty)
uv run poe type-check

# Run all unit tests
uv run poe unit-tests

# Run a single test file
uv run pytest tests/unit/path/to/test_file.py

# Run a single test by name
uv run pytest tests/unit/path/to/test_file.py::test_name -v

# Run tests with coverage XML report
uv run poe unit-tests-cov

# Build package
uv run poe build

# Clean build artifacts
uv run poe clean
```

Note: `uv run poe unit-tests` first runs tests marked `@pytest.mark.run_alone` in isolation, then runs the rest with `-x` (fail-fast) and parallelism via `pytest-xdist`.

## Code Style

- **Linter/formatter**: Ruff with `select = ["ALL"]` and specific ignores
- **Line length**: 120 characters
- **Quotes**: Single quotes (double for docstrings)
- **Docstrings**: Google format (enforced by Ruff)
- **Type checker**: ty (Astral's type checker), target Python 3.10
- **Async mode**: pytest-asyncio in `auto` mode (no need for `@pytest.mark.asyncio`)
- **Commits**: [Conventional Commits](https://www.conventionalcommits.org/) format. Choose the type based on *what* changed, not just *why*:
  - `feat:` / `fix:` / `perf:` / `refactor:` / `style:` — **source code only**; these trigger a release and appear in the changelog
  - `test:` — test additions or changes (no release triggered)
  - `docs:` — documentation changes; also triggers a doc release on master
  - `ci:` — CI/workflow changes
  - `chore:` — dependency bumps, tooling, and other housekeeping
  - `build:` — build system changes

## Architecture

### Crawler Hierarchy

```
BasicCrawler[TCrawlingContext, TStatisticsState]
├── AbstractHttpCrawler  →  HttpCrawler, BeautifulSoupCrawler, ParselCrawler
├── PlaywrightCrawler
└── AdaptivePlaywrightCrawler (extends PlaywrightCrawler)
```

- **BasicCrawler** (`src/crawlee/crawlers/_basic/`): Core request lifecycle, autoscaling pool, retries, session management, router dispatch. Generic over `TCrawlingContext`.
- **AbstractHttpCrawler** (`src/crawlee/crawlers/_abstract_http/`): Adds HTTP client integration, response parsing, pre-navigation hooks. Generic over parser result type.
- **PlaywrightCrawler** (`src/crawlee/crawlers/_playwright/`): Browser-based crawling with Playwright.

### Context Pipeline (Middleware Pattern)

Contexts are progressively enhanced through `ContextPipeline` middleware:

```
BasicCrawlingContext → HttpCrawlingContext → ParsedHttpCrawlingContext → BeautifulSoupCrawlingContext
```

Each middleware is an async generator that wraps the next handler, enabling setup/teardown around request processing.

### Storage Layer

Three-tier design:
- **High-level**: `Dataset`, `KeyValueStore`, `RequestQueue` in `src/crawlee/storages/`
- **Storage clients** (`src/crawlee/storage_clients/`): `FileSystemStorageClient` (default), `MemoryStorageClient`, `SqlStorageClient`, `RedisStorageClient`
- **Instance caching**: `StorageInstanceManager` is a global singleton that caches storage instances by ID/name

### Service Locator

`src/crawlee/_service_locator.py` is a global singleton managing `Configuration`, `EventManager`, `StorageClient`, and `StorageInstanceManager`. Prevents double-initialization with `ServiceConflictError`.

### HTTP Clients

Pluggable via `HttpClient` interface in `src/crawlee/http_clients/`:
- `ImpitHttpClient` (default), `HttpxHttpClient`, `CurlImpersonateHttpClient`
- Each provides `crawl()` (for crawler pipeline) and `send_request()` (for in-handler use)

### Request Model

`Request` (`src/crawlee/_request.py`) uses `unique_key` for deduplication. Lifecycle states: `UNPROCESSED → DONE`. Crawlee-specific metadata stored in `user_data['__crawlee']`.

### Router

```python
@crawler.router.default_handler
async def handler(context: BeautifulSoupCrawlingContext): ...

@crawler.router.handler(label='detail')
async def detail(context: BeautifulSoupCrawlingContext): ...
```

Requests are routed by their `label` field; unmatched requests go to the default handler.

### Key Directories

- `src/crawlee/crawlers/` - All crawler implementations
- `src/crawlee/storages/` - Dataset, KVS, RequestQueue
- `src/crawlee/storage_clients/` - Backend implementations
- `src/crawlee/http_clients/` - HTTP client implementations
- `src/crawlee/browsers/` - Playwright browser pool and plugins
- `src/crawlee/sessions/` - Session management with cookie persistence
- `src/crawlee/events/` - Event system (persist state, progress, aborting)
- `src/crawlee/_autoscaling/` - Autoscaled pool for concurrency control
- `src/crawlee/fingerprint_suite/` - Anti-bot fingerprint generation
- `src/crawlee/project_template/` - CLI scaffolding template (excluded from linting)
- `tests/unit/` - Unit tests
- `tests/e2e/` - End-to-end tests (require `apify-cli` + API token)
