Hands-on · ~20 min

Project Setup

Create the project skeleton, install dependencies, bring up Postgres in Docker, wire config & secrets. End of this chapter: a verifiable empty project ready for code.

Repo layout

Create the directory structure first.

shell
mkdir -p contract-intelligence/{data/{inbox,gold},src/ci/{extraction,eval},tests}
cd contract-intelligence
touch src/ci/__init__.py
touch src/ci/{config,db,models,ingestion,api}.py
touch src/ci/extraction/{__init__,ocr,llm,schemas}.py
touch src/ci/eval/{__init__,runner,scoring}.py
touch tests/{__init__,test_ingestion,test_extraction,test_api}.py

Dependencies

Pyproject.toml. Standard modern Python project; uv or poetry both work.

pyproject.toml
[project]
name = "contract-intelligence"
version = "0.1.0"
requires-python = ">=3.11"
dependencies = [
    "fastapi>=0.110",
    "uvicorn[standard]>=0.27",
    "sqlalchemy>=2.0",
    "psycopg[binary]>=3.1",
    "alembic>=1.13",
    "anthropic>=0.30",
    "pdfplumber>=0.10",
    "pytesseract>=0.3.10",
    "pillow>=10.0",
    "pdf2image>=1.17",
    "pydantic>=2.6",
    "pydantic-settings>=2.2",
    "rich>=13.7",
    "typer>=0.12",
]

[project.optional-dependencies]
dev = [
    "pytest>=8.0",
    "pytest-asyncio>=0.23",
    "httpx>=0.27",
    "mypy>=1.10",
    "ruff>=0.4",
]

[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]

[tool.mypy]
strict = true

Install:

python3 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"

# Also install Tesseract on the system (skip if you only have native PDFs)
# macOS:   brew install tesseract poppler
# Ubuntu:  sudo apt install tesseract-ocr poppler-utils

Postgres in Docker

One container, port 5432, persistent volume.

docker-compose.yml
services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: ci
      POSTGRES_PASSWORD: ci_dev
      POSTGRES_DB: contract_intel
    ports:
      - "5432:5432"
    volumes:
      - pgdata:/var/lib/postgresql/data
volumes:
  pgdata: {}

Bring it up:

docker compose up -d
# Verify
docker compose exec postgres psql -U ci -d contract_intel -c "SELECT version();"

Config & secrets

Pydantic-settings reads from environment + .env file.

src/ci/config.py
from pydantic_settings import BaseSettings, SettingsConfigDict


class Settings(BaseSettings):
    model_config = SettingsConfigDict(env_file=".env", env_prefix="CI_", extra="ignore")

    # Database
    db_dsn: str = "postgresql+psycopg://ci:ci_dev@localhost:5432/contract_intel"

    # LLM
    anthropic_api_key: str  # required, no default
    extraction_model: str = "claude-sonnet-4-6"
    extraction_temperature: float = 0.0
    extraction_max_tokens: int = 2048

    # Paths
    inbox_dir: str = "data/inbox"
    gold_dir: str = "data/gold"

    # Pipeline
    pipeline_version: str = "v0.1.0"
    customer_id: str = "demo_customer"


settings = Settings()
.env (gitignored)
CI_ANTHROPIC_API_KEY=sk-ant-...
# Override others as needed

Add .env to .gitignore.

Verify it works

Smoke test: import the module, instantiate settings, hit Postgres.

scripts/verify_setup.py
"""Verify project setup: imports work, Postgres reachable, LLM key present."""
from sqlalchemy import create_engine, text
from ci.config import settings

def main() -> None:
    print(f"Pipeline version: {settings.pipeline_version}")
    print(f"Model: {settings.extraction_model}")
    print(f"DB: {settings.db_dsn}")

    eng = create_engine(settings.db_dsn)
    with eng.connect() as conn:
        result = conn.execute(text("SELECT version();")).scalar()
        print(f"Postgres: {result}")

    assert settings.anthropic_api_key, "No Anthropic API key"
    print("✓ Setup OK.")

if __name__ == "__main__":
    main()

Run:

python scripts/verify_setup.py

You should see:

Pipeline version: v0.1.0
Model: claude-sonnet-4-6
DB: postgresql+psycopg://ci:ci_dev@localhost:5432/contract_intel
Postgres: PostgreSQL 16.x ...
✓ Setup OK.

If anything fails, troubleshoot before moving to chapter 02 — every later chapter assumes this works.