WP-0001-T001: service scaffold (Python, FastAPI, uv, ruff, mypy, pytest)

Lands the smallest credible foundation per ADR-0005:

- pyproject.toml: hatchling build, runtime deps (FastAPI, uvicorn, SQLAlchemy 2.0,
  asyncpg, aiosqlite, alembic, blake3, cbor2, typer, structlog, pydantic,
  pydantic-settings); dev deps (pytest, pytest-asyncio, httpx, hypothesis, ruff,
  mypy); ruff + mypy --strict + pytest configured.
- uv.lock committed.
- Makefile thin shims: install / dev / test / lint / format / type / migrate / clean.
- src/artifactstore/ package skeleton with placeholder __init__.py per concern:
  identity, manifest, events, retention, audit, storage, dataplane, registry,
  api/http (minimal FastAPI app, GET / scaffold banner), cli (typer app with
  version subcommand), config (pydantic-settings).
- tests/{unit,integration}/conftest.py present; unit smoke tests assert package
  imports, HTTP root route, CLI version round-trip, settings defaults.
- .env.example documents ARTIFACTSTORE_DATABASE_URL,
  ARTIFACTSTORE_STORAGE_LOCAL_ROOT, ARTIFACTSTORE_LOG_LEVEL.
- README updated with install / dev / test instructions.
- .gitignore: claude local state, local runtime data (var/, sqlite db).

make lint && make type && make test pass on a clean checkout (4 tests, 20
source files type-clean under mypy --strict).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 01:30:22 +02:00
parent b1eba9b41e
commit a6b6746f91
27 changed files with 1662 additions and 20 deletions

16
.env.example Normal file
View File

@@ -0,0 +1,16 @@
# artifact-store environment configuration
#
# Copy to .env for local development. All keys are prefixed with
# ARTIFACTSTORE_; pydantic-settings reads them automatically.
# Database connection string. SQLite (with aiosqlite) for dev; PostgreSQL
# (with asyncpg) for shared deployments.
# sqlite+aiosqlite:///./var/artifactstore.db
# postgresql+asyncpg://user:pass@host:5432/artifactstore
ARTIFACTSTORE_DATABASE_URL=sqlite+aiosqlite:///./var/artifactstore.db
# Local filesystem storage root. Used by the LocalBackend (WP-0001-T003).
ARTIFACTSTORE_STORAGE_LOCAL_ROOT=./var/storage
# Python logging level: DEBUG | INFO | WARNING | ERROR
ARTIFACTSTORE_LOG_LEVEL=INFO

8
.gitignore vendored
View File

@@ -174,3 +174,11 @@ cython_debug/
# PyPI configuration file
.pypirc
# Claude Code local state (ralph loop, local permissions)
.claude/*.local.*
# artifact-store local runtime data
/var/
artifactstore.db
artifactstore.db-journal

43
Makefile Normal file
View File

@@ -0,0 +1,43 @@
.PHONY: help install dev test lint format type migrate clean
help:
@echo "artifact-store — make targets"
@echo " install install / sync dependencies via uv"
@echo " dev run the FastAPI app with reload (uvicorn)"
@echo " test run the pytest suite"
@echo " lint ruff check + ruff format --check"
@echo " format ruff format (write changes)"
@echo " type mypy --strict over src and tests"
@echo " migrate alembic upgrade head (configured by WP-0001-T002)"
@echo " clean remove caches and build artefacts"
install:
uv sync --all-extras
dev:
uv run uvicorn artifactstore.api.http:app --reload --host 127.0.0.1 --port 8000
test:
uv run pytest
lint:
uv run ruff check .
uv run ruff format --check .
format:
uv run ruff format .
uv run ruff check --fix .
type:
uv run mypy
migrate:
@if [ -f alembic.ini ]; then \
uv run alembic upgrade head; \
else \
echo "alembic.ini not present yet — see ARTIFACT-STORE-WP-0001-T002"; \
fi
clean:
rm -rf .pytest_cache .mypy_cache .ruff_cache build dist *.egg-info
find . -type d -name __pycache__ -prune -exec rm -rf {} +

View File

@@ -5,9 +5,8 @@ evidence packages, reports, logs, snapshots, exports, and release
artifacts.
The registry owns artifact identity, metadata, provenance, retention
policy, and retrieval records. Bytes are delegated to configured
storage backends (local filesystem in v1, S3-compatible / Ceph RGW
next).
policy, and retrieval records. Bytes are delegated to configured storage
backends (local filesystem in v1, S3-compatible / Ceph RGW next).
The shape is library-first (`artifactstore` Python package); the HTTP
server and the CLI are thin consumers. Content is addressed by digest;
@@ -16,26 +15,40 @@ are rebuildable.
## Status
Concept / service-baseline planning. No runnable scaffold yet —
`workplans/ARTIFACT-STORE-WP-0001-service-baseline.md` is the next step.
Scaffold landed. The core kernels and local FS backend follow in the
remaining tasks of [WP-0001](workplans/ARTIFACT-STORE-WP-0001-service-baseline.md).
## Start here
## Develop
Requires Python ≥ 3.12 and [`uv`](https://docs.astral.sh/uv/) on the path.
```sh
make install # uv sync --all-extras
cp .env.example .env
make dev # uvicorn artifactstore.api.http:app --reload
make test # pytest
make lint # ruff check + ruff format --check
make type # mypy --strict
make migrate # alembic upgrade head (configured in WP-0001-T002)
```
The dev server listens on `127.0.0.1:8000`. The scaffold root route
returns `{"service": "artifact-store", "status": "scaffold"}`; the real
`/health` endpoint lands in `WP-0001-T014`.
## Documentation
- [INTENT.md](INTENT.md) — purpose, product thesis, scope, boundary.
- [SCOPE.md](SCOPE.md) — lightweight orientation.
- [docs/ARCHITECTURE-BLUEPRINT.md](docs/ARCHITECTURE-BLUEPRINT.md) — the
v2 architecture: modules, data model, API shape.
- [docs/ARCHITECTURE-BLUEPRINT.md](docs/ARCHITECTURE-BLUEPRINT.md) — v2
architecture: modules, data model, API shape.
- [docs/PLATFORM-AMBITION.md](docs/PLATFORM-AMBITION.md) — longer-horizon
thesis, ffmpeg / VLC reference points, the schema commitments v1
preserves.
- [docs/ROADMAP.md](docs/ROADMAP.md) — workplan sequencing across
phases.
- [docs/adr/](docs/adr/) — architecture decision records (ADR-0001 …
ADR-0006).
thesis and v1 schema commitments.
- [docs/ROADMAP.md](docs/ROADMAP.md) — workplan sequencing across phases.
- [docs/adr/](docs/adr/) — architecture decision records.
- [docs/ASSEMBLY-EXPERIMENT.md](docs/ASSEMBLY-EXPERIMENT.md) — opt-in
research line on hand-tuned assembly for hot kernels.
- [docs/REVIEW-2026-05-15-intent-and-blueprint.md](docs/REVIEW-2026-05-15-intent-and-blueprint.md)
— the SWOT review that triggered this cleanup.
research line on hand-tuned asm for hot kernels.
## Active workplans
@@ -47,5 +60,5 @@ Concept / service-baseline planning. No runnable scaffold yet —
## Agent operating notes
See [AGENTS.md](AGENTS.md) for the StateHub-integrated session
protocol, workplan conventions, and progress-logging contract.
See [AGENTS.md](AGENTS.md) for the StateHub-integrated session protocol,
workplan conventions, and progress-logging contract.

99
pyproject.toml Normal file
View File

@@ -0,0 +1,99 @@
[build-system]
requires = ["hatchling>=1.25"]
build-backend = "hatchling.build"
[project]
name = "artifactstore"
version = "0.1.0"
description = "Generic artifact registry and storage gateway"
readme = "README.md"
requires-python = ">=3.12"
license = { file = "LICENSE" }
authors = [{ name = "artifact-store contributors" }]
keywords = ["artifact", "storage", "registry", "evidence", "retention"]
classifiers = [
"Development Status :: 2 - Pre-Alpha",
"Intended Audience :: Developers",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Software Development :: Libraries",
"Topic :: System :: Archiving",
]
dependencies = [
"fastapi >= 0.115",
"uvicorn[standard] >= 0.30",
"sqlalchemy >= 2.0",
"asyncpg >= 0.29",
"aiosqlite >= 0.20",
"alembic >= 1.13",
"blake3 >= 0.4",
"cbor2 >= 5.6",
"typer >= 0.12",
"structlog >= 24.1",
"pydantic >= 2.7",
"pydantic-settings >= 2.4",
]
[project.optional-dependencies]
dev = [
"pytest >= 8.0",
"pytest-asyncio >= 0.23",
"httpx >= 0.27",
"hypothesis >= 6.100",
"ruff >= 0.6",
"mypy >= 1.10",
]
[project.scripts]
artifactstore = "artifactstore.cli:app"
[tool.hatch.build.targets.wheel]
packages = ["src/artifactstore"]
[tool.uv]
dev-dependencies = [
"pytest >= 8.0",
"pytest-asyncio >= 0.23",
"httpx >= 0.27",
"hypothesis >= 6.100",
"ruff >= 0.6",
"mypy >= 1.10",
]
[tool.ruff]
target-version = "py312"
line-length = 100
src = ["src", "tests"]
extend-exclude = [".venv", "var", "migrations"]
[tool.ruff.lint]
select = ["E", "F", "W", "I", "N", "UP", "B", "C4", "RET", "SIM", "RUF"]
ignore = [
"B008", # FastAPI dependency injection idiom
]
[tool.ruff.lint.per-file-ignores]
"tests/**" = ["S101"]
"src/artifactstore/cli/__init__.py" = ["UP007"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
[tool.mypy]
python_version = "3.12"
strict = true
files = ["src", "tests"]
mypy_path = "src"
explicit_package_bases = true
namespace_packages = true
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
pythonpath = ["src"]
addopts = "-q --strict-markers"
markers = [
"integration: marks tests as integration (requires DB / backend)",
]

View File

@@ -0,0 +1,8 @@
"""artifact-store: generic artifact registry and storage gateway.
The top-level package re-exports nothing yet. Submodules are documented in
``docs/ARCHITECTURE-BLUEPRINT.md`` and each carries its own placeholder for
the implementation tasks in workplan ARTIFACT-STORE-WP-0001.
"""
__version__ = "0.1.0"

View File

@@ -0,0 +1,4 @@
"""HTTP and future RPC API surfaces.
Submodules: :mod:`artifactstore.api.http`.
"""

View File

@@ -0,0 +1,24 @@
"""FastAPI application entry point.
The full registry-aware ``/health`` endpoint lands in
ARTIFACT-STORE-WP-0001-T014. This scaffold exposes a minimal root route so
``make dev`` has something to serve while the registry layer is being built.
"""
from __future__ import annotations
from fastapi import FastAPI
from artifactstore import __version__
app = FastAPI(title="artifact-store", version=__version__)
@app.get("/")
def root() -> dict[str, str]:
"""Return a service banner indicating the scaffold is up."""
return {
"service": "artifact-store",
"version": __version__,
"status": "scaffold",
}

View File

@@ -0,0 +1,5 @@
"""Audit surface over the event log.
Audit is a view over :mod:`artifactstore.events` filtered to access and
lifecycle event types; it has no separate write path.
"""

View File

@@ -0,0 +1,36 @@
"""artifact-store command-line interface.
The CLI is a thin consumer of :mod:`artifactstore.registry` (per ADR-0005).
The scaffold exposes only ``version``; richer subcommands land in later
tasks of ARTIFACT-STORE-WP-0001 and follow-on workplans.
"""
from __future__ import annotations
import typer
from artifactstore import __version__
app = typer.Typer(
help="artifact-store: artifact registry and storage gateway",
no_args_is_help=True,
)
@app.callback()
def main() -> None:
"""Top-level CLI entry point.
Forces typer into multi-command mode so subcommands behave consistently
even while the scaffold only ships ``version``.
"""
@app.command()
def version() -> None:
"""Print the artifactstore version and exit."""
typer.echo(__version__)
if __name__ == "__main__": # pragma: no cover
app()

View File

@@ -0,0 +1,30 @@
"""Application configuration loaded from environment variables.
All settings are read from environment variables prefixed with
``ARTIFACTSTORE_``. A ``.env`` file at the repository root is honoured for
local development; see ``.env.example``.
"""
from __future__ import annotations
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
"""Top-level service configuration."""
model_config = SettingsConfigDict(
env_prefix="ARTIFACTSTORE_",
env_file=".env",
env_file_encoding="utf-8",
extra="ignore",
)
database_url: str = "sqlite+aiosqlite:///./var/artifactstore.db"
storage_local_root: str = "./var/storage"
log_level: str = "INFO"
def get_settings() -> Settings:
"""Return a freshly-loaded :class:`Settings` instance."""
return Settings()

View File

@@ -0,0 +1,5 @@
"""Data plane SPI and in-process implementation.
The SPI lands in ARTIFACT-STORE-WP-0001-T012. See ADR-0004 for the
control-plane / data-plane contract that this module isolates.
"""

View File

@@ -0,0 +1,5 @@
"""Append-only event log and materialised-view replayer.
Real implementation lands in ARTIFACT-STORE-WP-0001-T011. See ADR-0002 for
the event-log-as-source-of-truth contract.
"""

View File

@@ -0,0 +1,5 @@
"""Content addresses and digest abstraction.
Real implementation lands in ARTIFACT-STORE-WP-0001-T009. See ADR-0001 for
the dual-digest contract (BLAKE3 primary, SHA-256 retained for interop).
"""

View File

@@ -0,0 +1,5 @@
"""Package manifest model and canonical-CBOR codec.
Real implementation lands in ARTIFACT-STORE-WP-0001-T010. See ADR-0003 for
the canonicalisation pin (RFC 8949 §4.2.2).
"""

View File

@@ -0,0 +1,6 @@
"""Registry orchestrator.
Real implementation lands in ARTIFACT-STORE-WP-0001-T013. The orchestrator
combines identity, manifest, events, retention, and dataplane into the
operations exposed by the HTTP API and CLI.
"""

View File

@@ -0,0 +1,5 @@
"""Retention policy engine.
Seed classes land in ARTIFACT-STORE-WP-0001-T002 (data model). Active policy
operations (extensions, holds, sweeper) land in workplan WP-0003.
"""

View File

@@ -0,0 +1,5 @@
"""Storage adapter SPI and backend registry.
The SPI and local filesystem backend land in ARTIFACT-STORE-WP-0001-T003.
The S3-compatible backend lands in workplan WP-0004.
"""

0
tests/__init__.py Normal file
View File

5
tests/conftest.py Normal file
View File

@@ -0,0 +1,5 @@
"""Top-level pytest configuration.
Shared fixtures land in this module as the test suite grows. The scaffold
keeps it empty so unit and integration suites configure themselves.
"""

View File

View File

@@ -0,0 +1,5 @@
"""Integration-test pytest configuration.
Integration suites require a database and storage backend; configuration
fixtures land here as those layers come online in later tasks.
"""

0
tests/unit/__init__.py Normal file
View File

1
tests/unit/conftest.py Normal file
View File

@@ -0,0 +1 @@
"""Unit-test pytest configuration."""

35
tests/unit/test_smoke.py Normal file
View File

@@ -0,0 +1,35 @@
"""Smoke tests asserting the scaffold imports cleanly."""
from __future__ import annotations
from typer.testing import CliRunner
import artifactstore
from artifactstore.api.http import app as http_app
from artifactstore.cli import app as cli_app
from artifactstore.config import Settings, get_settings
def test_package_version_exposed() -> None:
assert isinstance(artifactstore.__version__, str)
assert artifactstore.__version__
def test_http_app_root_route_registered() -> None:
routes = {getattr(r, "path", None) for r in http_app.routes}
assert "/" in routes
def test_cli_version_command_round_trips() -> None:
runner = CliRunner()
result = runner.invoke(cli_app, ["version"])
assert result.exit_code == 0, result.output
assert artifactstore.__version__ in result.output
def test_settings_defaults_loadable() -> None:
settings = get_settings()
assert isinstance(settings, Settings)
assert settings.log_level
assert settings.database_url
assert settings.storage_local_root

1274
uv.lock generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -66,7 +66,7 @@ cli (just `artifactstore version`, `artifactstore migrate`, `artifactstore repla
```task
id: ARTIFACT-STORE-WP-0001-T001
status: todo
status: done
priority: high
state_hub_task_id: "84209430-ec3b-4c5e-924e-019c25434230"
```