Development/Packaging
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).