Development/Packaging

From Dura Lex Wiki
Jump to navigation Jump to search

Packaging Conventions

[edit | edit source]

All duralex-* packages follow these conventions for consistent packaging, installation, and namespace management.

Naming

[edit | edit source]
Layer Convention Example
Brand Dura Lex (always two words)
Domain dura-lex.* (hyphenated) dura-lex.org
GitHub org duralex github.com/duralex/
PyPI package duralex-{name} (hyphenated) duralex-fr, duralex-mcp
Python import duralex.{name} (dotted) from duralex.corpus import ...
Exception The core package is just duralex on PyPI pip install duralex

Repository structure (6 repos)

[edit | edit source]
~/duralex/
├── duralex/                          # PyPI: duralex (core + corpus + specs)
│   ├── pyproject.toml
│   ├── src/duralex/
│   │   ├── corpus/                   # DocumentStore, FTS, TagQuery
│   │   ├── temporal/                 # versioning, temporal types
│   │   ├── data/                     # citation types, text utils
│   │   ├── annotations/             # annotation framework
│   │   ├── concepts/                # concept definitions
│   │   ├── errors.py                # DuralexError base
│   │   ├── database.py              # DSN builder
│   │   ├── logging.py               # JSON/console logging
│   │   ├── middleware.py             # ASGI middleware
│   │   └── retry.py                 # exponential backoff
│   ├── tests/
│   ├── sql/                          # corpus DDL
│   ├── spec/                         # authoritative specifications
│   ├── coding-conventions/           # this directory
│   ├── design-decisions/             # ADRs
│   ├── research/                     # investigation logs
│   └── issues/                       # issue tracker
│
├── duralex-jurisdictions/            # monorepo, two PyPI packages
│   ├── duralex-fr/                   # PyPI: duralex-fr
│   │   ├── pyproject.toml
│   │   ├── src/duralex/fr/
│   │   └── tests/
│   └── duralex-eu/                   # PyPI: duralex-eu
│       ├── pyproject.toml
│       ├── src/duralex/eu/
│       └── tests/
│
├── duralex-ingest/                   # monorepo, three PyPI packages
│   ├── duralex-ingest/               # PyPI: duralex-ingest
│   │   ├── pyproject.toml
│   │   ├── src/duralex/ingest/       # NO __init__.py (namespace)
│   │   └── tests/
│   ├── duralex-ingest-fr/            # PyPI: duralex-ingest-fr
│   │   ├── pyproject.toml
│   │   ├── src/duralex/ingest/fr/
│   │   └── tests/
│   └── duralex-ingest-eu/            # PyPI: duralex-ingest-eu
│       ├── pyproject.toml
│       ├── src/duralex/ingest/eu/
│       └── tests/
│
├── duralex-mcp/                      # PyPI: duralex-mcp (+ Docker infra)
│   ├── pyproject.toml
│   ├── src/duralex/mcp/
│   ├── tests/
│   ├── Dockerfile
│   ├── docker-compose.dev.yml
│   ├── docker-compose.prod.yml
│   └── Caddyfile
│
├── duralex-portal/                   # PyPI: duralex-portal
│   ├── pyproject.toml
│   ├── src/duralex/portal/
│   └── tests/
│
└── duralex-feedback/                 # standalone, not distributed
    ├── server.py
    ├── Dockerfile
    └── docker-compose.yml

src layout with namespace packages

[edit | edit source]

Every package uses the src layout with implicit namespace packages (PEP 420).

The critical rule: no __init__.py in namespace directories. Only leaf packages have __init__.py.

duralex-fr/
├── pyproject.toml
├── src/
│   └── duralex/                     # NO __init__.py (namespace)
│       └── fr/                      # NO __init__.py (namespace)
│           ├── mcp/
│           │   ├── __init__.py      # leaf package
│           │   └── ...
│           ├── refs/
│           │   ├── __init__.py
│           │   └── ...
│           └── courts/
│               ├── __init__.py
│               └── ...
└── tests/

Special case: duralex-ingest namespace

[edit | edit source]

The duralex.ingest namespace is shared across three packages. The base duralex-ingest package must NOT have __init__.py at src/duralex/ingest/ — otherwise Python blocks namespace discovery of duralex.ingest.fr and duralex.ingest.eu from sibling packages.

duralex-ingest/duralex-ingest/src/duralex/ingest/       # NO __init__.py
duralex-ingest/duralex-ingest/src/duralex/ingest/database/__init__.py   # leaf
duralex-ingest/duralex-ingest-fr/src/duralex/ingest/fr/__init__.py      # leaf
duralex-ingest/duralex-ingest-eu/src/duralex/ingest/eu/__init__.py      # leaf

pyproject.toml template

[edit | edit source]

<syntaxhighlight lang="toml"> [build-system] requires = ["setuptools>=69.0"] build-backend = "setuptools.build_meta"

[project] name = "duralex-fr" version = "0.1.0" description = "France: legal reference resolver, court hierarchy, and validation for the Dura Lex ecosystem" requires-python = ">=3.12" license = "MIT" authors = [{ name = "Nicolas Baldeck" }] keywords = ["legal", "open-data", "france"]

dependencies = [

   "duralex>=0.1.0",

]

[project.optional-dependencies] dev = [

   "pytest>=8.2",
   "ruff>=0.15",
   "mypy>=1.15",

]

[tool.setuptools.packages.find] where = ["src"] include = ["duralex.fr*"]

[tool.ruff] line-length = 120 target-version = "py312"

[tool.ruff.lint] select = ["E", "F", "W", "I", "UP", "B", "SIM", "TCH", "RUF", "PTH"]

[tool.ruff.format] quote-style = "double"

[tool.mypy] python_version = "3.12" strict = true namespace_packages = true explicit_package_bases = true mypy_path = ["src", "../../duralex/src"]

[tool.pytest.ini_options] testpaths = ["tests"] addopts = ["--strict-config", "--strict-markers", "--import-mode=importlib", "-m not slow and not integration"] xfail_strict = true consider_namespace_packages = true filterwarnings = ["error"] markers = [

   "slow: tests with extended runtime",
   "integration: requires PostgreSQL (testcontainers)",

] </syntaxhighlight>

Editable development installs

[edit | edit source]

Install all packages in editable mode from the repo root:

<syntaxhighlight lang="bash"> cd ~/duralex pip install -e "./duralex[dev,pg]" pip install -e "./duralex-jurisdictions/duralex-fr[dev,db]" pip install -e "./duralex-jurisdictions/duralex-eu[dev]" pip install -e "./duralex-ingest/duralex-ingest[dev]" pip install -e "./duralex-ingest/duralex-ingest-fr[dev]" pip install -e "./duralex-ingest/duralex-ingest-eu[dev]" pip install -e "./duralex-mcp[dev]" pip install -e "./duralex-portal[dev]" </syntaxhighlight>

If namespace resolution breaks between sibling packages:

<syntaxhighlight lang="bash"> pip install -e ".[dev]" --config-settings editable_mode=compat </syntaxhighlight>

Version policy

[edit | edit source]

All packages start at 0.1.0. Packages are versioned independently. A major version bump in duralex triggers compatibility review in all downstream packages.

Brand vs. code

[edit | edit source]

The brand is always written Dura Lex (two words). Code identifiers use duralex (one word, no space) because Python and PyPI do not allow spaces. Never write "Duralex" — it is either "Dura Lex" (prose) or duralex (code).