diff --git a/.github/workflows/python-build.yml b/.github/workflows/python-build.yml index 46cd9ac8..c6ca602d 100644 --- a/.github/workflows/python-build.yml +++ b/.github/workflows/python-build.yml @@ -18,4 +18,4 @@ jobs: python-version-file: ".python-version" - name: Build - run: make build-python + run: make py-build diff --git a/.github/workflows/python-format.yml b/.github/workflows/python-format.yml index 5644375f..979ce69c 100644 --- a/.github/workflows/python-format.yml +++ b/.github/workflows/python-format.yml @@ -18,5 +18,4 @@ jobs: python-version-file: ".python-version" - name: Run format check - run: make uv-format-check - + run: make py-format-check diff --git a/.github/workflows/python-lint.yml b/.github/workflows/python-lint.yml index 9826cb34..69eec0dc 100644 --- a/.github/workflows/python-lint.yml +++ b/.github/workflows/python-lint.yml @@ -18,4 +18,4 @@ jobs: python-version-file: ".python-version" - name: Run Linter - run: make uv-lint + run: make py-lint diff --git a/.github/workflows/python-lock-check.yml b/.github/workflows/python-lock-check.yml index 82c4e5f5..6abe6fe9 100644 --- a/.github/workflows/python-lock-check.yml +++ b/.github/workflows/python-lock-check.yml @@ -18,4 +18,4 @@ jobs: python-version-file: ".python-version" - name: Lock check - run: make uv-lock-check + run: make py-lock-check diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml index 04753097..fab2eefd 100644 --- a/.github/workflows/python-test.yml +++ b/.github/workflows/python-test.yml @@ -18,4 +18,4 @@ jobs: python-version-file: ".python-version" - name: Run Tests - run: make uv-test-isolated + run: make py-test-exact diff --git a/HACKING.md b/HACKING.md index e441c58c..a8be3702 100644 --- a/HACKING.md +++ b/HACKING.md @@ -1,85 +1,173 @@ # Hacking on Stitch -This guide covers day-to-day development in the current Stitch monorepo. +This guide covers the day-to-day development workflow in the Stitch monorepo. ## Monorepo layout -- `packages/stitch-auth`: auth/claims/validation package -- `deployments/api`: FastAPI service (`stitch-api`) -- `deployments/stitch-frontend`: React + Vite frontend -- `deployments/db`: DB bootstrap/role scripts +* `deployments/api` — FastAPI service (`stitch-api`) +* `deployments/stitch-frontend` — React + Vite frontend +* `deployments/db` — database bootstrap and role scripts +* `deployments/seed` — local seed data tooling +* `packages/` — shared Python packages ## Prerequisites -- Docker Desktop (`docker`, `docker compose`) -- `uv` for Python dependency/workspace management -- Node.js + npm (for frontend-only workflows) +* Docker Desktop (`docker`, `docker compose`) +* `uv` for Python dependency and workspace management +* Node.js + npm for frontend development ## First-time setup +From the repo root: + ```bash cp env.example .env +make uv-sync-dev +``` + +You can also run the equivalent `uv` command directly: + +```bash uv sync --group dev --all-packages ``` -If you are running the full stack via Docker, `uv sync` is optional unless you also run tests/tools on host. +## The main entrypoints + +In most cases, start with one of these three commands: + +* `make frontend-dev` +* `make api-dev` +* `make dev-docker` (or `make reboot-docker`) + +### `make frontend-dev` + +Use this when you are primarily working on the frontend. + +This target: + +* installs frontend dependencies if needed +* starts the supporting local services in Docker +* runs the Vite dev server on the host + +The API and supporting services run in Docker, while the frontend runs locally with fast rebuilds. + +```bash +make frontend-dev +``` + +### `make api-dev` + +Use this when you are primarily working on the API. + +This target: -## Run the stack (recommended) +* starts the supporting local services in Docker +* runs the FastAPI app on the host with reload enabled + +The frontend and supporting services run in Docker, while the API runs locally for a tighter backend loop. ```bash -docker compose up --build +make api-dev ``` -Or: +### `make dev-docker` + +Use this when you want the whole local stack running in Docker. + +This is the best choice when you want the most production-like local setup or do not need to run either app directly on the host. ```bash make dev-docker ``` -Useful local URLs: +## Which one should I use? + +A simple rule of thumb: + +* changing React/UI code: `make frontend-dev` +* changing FastAPI/backend code: `make api-dev` +* validating the whole stack together: `make dev-docker` + +## Useful local URLs + +Depending on which entrypoint you use, these are the main local endpoints: + +* Frontend: `http://localhost:3000` +* API docs: `http://localhost:8000/docs` +* Adminer: `http://localhost:8081` + +## Other useful targets + +You do not need to memorize the whole Makefile, but these are worth knowing. -- Frontend: http://localhost:3000 -- API docs: http://localhost:8000/docs -- Adminer: http://localhost:8081 +### `make check` -## Common dev commands +Runs the main verification suite in one command: -Run from repo root. +* lint +* tests +* format checks +* lockfile checks + +Use this before pushing or when you want a quick confidence pass. ```bash -make lint -make test -make format -make format-check make check ``` -Python-only: +### `make clean` + +Resets local build artifacts, caches, frontend install/build outputs, and Docker state. + +⚠️ **Warning:** this target includes `clean-docker`, which removes Docker containers **and volumes**. This will delete your local database data. + +Use this when you want a completely fresh environment. ```bash -uv run ruff check -uv run ruff format -uv run pytest deployments/api +make clean ``` -Frontend-only: +### `make reboot-docker` + +Performs a clean Docker reset and immediately brings the full stack back up with builds. + +This is a convenience target for the common “wipe it and restart everything” workflow. ```bash -npm --prefix deployments/stitch-frontend ci -npm --prefix deployments/stitch-frontend run dev -npm --prefix deployments/stitch-frontend run lint -npm --prefix deployments/stitch-frontend run test:run +make reboot-docker ``` -## Reset local DB data + +### `make follow-stack-logs` + +Follows logs for the full Docker-based local stack. + +Useful when debugging service startup or cross-service interactions. ```bash -docker compose down -v -docker compose up db api frontend +make follow-stack-logs ``` -Or: +## Common quality commands + +From the repo root: ```bash -make clean-docker -make dev-docker +make lint +make test +make format +make format-check +make lock-check +make check ``` + +## When things get weird + +A good escalation path is: + +```bash +make check +make clean +make reboot-docker +``` + +That sequence catches most local issues caused by stale caches, stale frontend installs, or stale Docker volumes. diff --git a/Makefile b/Makefile index a2ff9ab9..2385d52b 100644 --- a/Makefile +++ b/Makefile @@ -9,74 +9,131 @@ TEST_PKG := ./scripts/test-package.py check: lint test format-check lock-check @echo "All checks passed." -lint: uv-lint frontend-lint +lint: py-lint frontend-lint +test: py-test frontend-test +format-check: py-format-check frontend-format-check +lock-check: py-lock-check -test: uv-test frontend-test +format: py-format frontend-format +clean: clean-build py-clean-cache frontend-clean clean-docker -format: uv-format frontend-format +build-all: py-build frontend-build -format-check: uv-format-check frontend-format-check - -lock-check: uv-lock-check +clean-build: + rm -rf build dist -uv-lint: uv-dev - $(RUFF) check +# --------------------------------------------------------------------- +# Python (UV) infrastructure +# --------------------------------------------------------------------- -# All workspace packages with tests -TEST_PACKAGES := stitch-api stitch-models +uv-dev: uv-sync-dev +uv-sync-dev: + $(UV) sync --group dev --all-packages -define newline +py-lint: uv-dev + $(RUFF) check -endef +py-test: api-test pkg-test +py-test-exact: api-test-exact pkg-test-exact -# --- local: full sync, no --exact (fast, no venv mutation) --- -ifdef pkg -uv-test: uv-dev - $(TEST_PKG) $(pkg) -else -uv-test: uv-dev - $(foreach p,$(TEST_PACKAGES),$(TEST_PKG) $(p)$(newline)) -endif +py-format-check: uv-dev + $(RUFF) format --check -# --- isolated (CI): per-package --exact deps only --- -ifdef pkg -uv-test-isolated: - $(TEST_PKG) --exact $(pkg) -else -uv-test-isolated: - $(foreach p,$(TEST_PACKAGES),$(TEST_PKG) --exact $(p)$(newline)) -endif +py-lock-check: + $(UV) lock --check -uv-format: uv-dev +py-format: uv-dev $(RUFF) format -uv-format-check: uv-dev - $(RUFF) format --check +py-clean-cache: + rm -rf .ruff_cache .pytest_cache -uv-dev: uv-sync-dev +py-build: api-build pkg-build uv-sync: $(UV) sync -uv-sync-dev: - $(UV) sync --group dev --all-packages +# Generic helpers +uv-test-target: + $(UV) run --package $(PKG) --active pytest $(TEST_PATH) $(ARGS) -uv-lock-check: - $(UV) lock --check +uv-test-target-exact: + $(UV) run --package $(PKG) --active --exact --group dev pytest $(TEST_PATH) $(ARGS) # --------------------------------------------------------------------- -# Packages and source discovery +# UV Packages # --------------------------------------------------------------------- -all: build-python frontend -build-python: -clean: clean-build clean-cache frontend-clean clean-docker -clean-build: - rm -rf build dist -clean-cache: - rm -rf .ruff_cache .pytest_cache +pkg-build-auth: + $(UV) build --package stitch-auth +pkg-test-auth: + $(MAKE) uv-test-target PKG=stitch-auth TEST_PATH=packages/stitch-auth +pkg-test-exact-auth: + $(MAKE) uv-test-target-exact PKG=stitch-auth TEST_PATH=packages/stitch-auth + +pkg-build-models: + $(UV) build --package stitch-models +pkg-test-models: + $(MAKE) uv-test-target PKG=stitch-models TEST_PATH=packages/stitch-models +pkg-test-exact-models: + $(MAKE) uv-test-target-exact PKG=stitch-models TEST_PATH=packages/stitch-models + +pkg-build-ogsi: + $(UV) build --package stitch-ogsi +pkg-test-ogsi: + $(MAKE) uv-test-target PKG=stitch-ogsi TEST_PATH=packages/stitch-ogsi +pkg-test-exact-ogsi: + $(MAKE) uv-test-target-exact PKG=stitch-ogsi TEST_PATH=packages/stitch-ogsi + +pkg-build: pkg-build-auth pkg-build-models pkg-build-ogsi +pkg-test: pkg-test-auth pkg-test-models pkg-test-ogsi +pkg-test-exact: pkg-test-exact-auth pkg-test-exact-models pkg-test-exact-ogsi +# --------------------------------------------------------------------- +# Deployments +# --------------------------------------------------------------------- + +api-build: + $(UV) build --package stitch-api +api-test: + $(MAKE) uv-test-target PKG=stitch-api TEST_PATH=deployments/api +api-test-exact: + $(MAKE) uv-test-target-exact PKG=stitch-api TEST_PATH=deployments/api + +api-dev: stack-api-dev + POSTGRES_HOST=127.0.0.1 \ + POSTGRES_USER=stitch_app \ + $(UV) run --env-file .env -- \ + uvicorn stitch.api.main:app \ + --host 0.0.0.0 \ + --port 8000 \ + --reload \ + --reload-dir deployments/api/src \ + --reload-dir packages \ + --reload-exclude '*/tests/*' + +stack-api-dev: + SEED_API_BASE_URL=http://host.docker.internal:8000/api/v1 \ + $(DOCKER_COMPOSE_DEV) \ + --profile frontend \ + --profile tools \ + --profile seed \ + up --build \ + -d + +frontend-dev: $(FRONTEND_INSTALL_STAMP) stack-frontend-dev + VITE_API_URL=http://localhost:8000/api/v1 \ + $(NPM) run dev + +stack-frontend-dev: + SEED_API_BASE_URL=http://api:8000/api/v1 \ + $(DOCKER_COMPOSE_DEV) \ + --profile api \ + --profile tools \ + --profile seed \ + up --build \ + -d # --------------------------------------------------------------------- # stitch-frontend @@ -131,33 +188,51 @@ frontend-format: $(FRONTEND_INSTALL_STAMP) frontend-format-check: $(FRONTEND_INSTALL_STAMP) $(NPM) run format:check -frontend-dev: $(FRONTEND_INSTALL_STAMP) - $(NPM) run dev - frontend-clean: rm -rf $(FRONTEND_DIR)/dist $(FRONTEND_DIR)/node_modules \ $(FRONTEND_INSTALL_STAMP) $(FRONTEND_BUILD_STAMP) -# docker +# --------------------------------------------------------------------- +# Docker +# --------------------------------------------------------------------- clean-docker: - $(DOCKER_COMPOSE_DEV) down --volumes --remove-orphans + $(DOCKER_COMPOSE_DEV) --profile "*" down --volumes --remove-orphans dev-docker: - $(DOCKER_COMPOSE_DEV) up + $(DOCKER_COMPOSE_DEV) --profile full up reboot-docker: clean-docker - $(DOCKER_COMPOSE_DEV) up --build - -prod-docker: - $(DOCKER_COMPOSE) up - -.PHONY: all build clean \ - build-python \ - check lint test format format-check \ - uv-lint uv-test uv-test-isolated uv-format uv-format-check \ - uv-sync uv-sync-dev uv-sync-all \ - uv-dev \ - clean-build clean-cache \ - lock-check uv-lock-check \ - clean-docker dev-docker \ - frontend frontend-install frontend-build frontend-test frontend-lint frontend-dev frontend-clean frontend-format frontend-format-check + $(DOCKER_COMPOSE_DEV) --profile full up --build + +follow-stack-logs: + $(DOCKER_COMPOSE_DEV) --profile full logs -f + +.PHONY: \ + # Workspace + check lint test format format-check lock-check \ + build-all \ + clean clean-build \ + \ + # Python (uv) + py-lint py-test py-test-exact py-format py-format-check py-lock-check py-clean-cache \ + py-build \ + uv-dev uv-sync uv-sync-dev \ + uv-test-target uv-test-target-exact \ + \ + # Packages + pkg-test pkg-test-exact \ + pkg-build-auth pkg-test-auth pkg-test-exact-auth \ + pkg-build-models pkg-test-models pkg-test-exact-models \ + pkg-build-ogsi pkg-test-ogsi pkg-test-exact-ogsi \ + \ + # API + api-build api-test api-test-exact api-dev stack-api-dev \ + \ + # Frontend + frontend frontend-install frontend-build frontend-test frontend-lint \ + frontend-format frontend-format-check \ + frontend-dev frontend-clean \ + \ + # Docker + clean-docker dev-docker reboot-docker \ + stack-frontend-dev follow-stack-logs diff --git a/deployments/seed/src/stitch/seed/__main__.py b/deployments/seed/src/stitch/seed/__main__.py index 6a430674..255feb8e 100644 --- a/deployments/seed/src/stitch/seed/__main__.py +++ b/deployments/seed/src/stitch/seed/__main__.py @@ -1,5 +1,5 @@ import httpx -from .client import post_payloads +from .client import post_payloads, wait_for_api from .config import configure_logging, load_config, logger from .openapi_validate import OpenAPIRequestValidator from .payloads import iter_payloads @@ -13,6 +13,8 @@ def main() -> None: logger.info("API_BASE_URL=%s", cfg.api_base_url) logger.info("FAKER_POST_COUNT=%s", cfg.faker_post_count) + wait_for_api(cfg.api_base_url) + validator = OpenAPIRequestValidator(cfg.api_base_url, openapi_url=cfg.openapi_url) payloads = iter_payloads( static_payload_dir=cfg.static_payload_dir, diff --git a/deployments/seed/src/stitch/seed/client.py b/deployments/seed/src/stitch/seed/client.py index ebce060e..f2d2348f 100644 --- a/deployments/seed/src/stitch/seed/client.py +++ b/deployments/seed/src/stitch/seed/client.py @@ -5,6 +5,8 @@ from typing import Any, Iterable import httpx +import time + from .openapi_validate import OpenAPIRequestValidator @@ -41,3 +43,28 @@ def post_payloads( ) resp.raise_for_status() + + +def wait_for_api(base_url: str, retries: int = 30, delay: float = 2.0) -> None: + url = f"{base_url.rstrip('/')}/health" + logger.info("url: %s", url) + + for attempt in range(1, retries + 1): + try: + r = httpx.get(url, timeout=2.0) + if 200 <= r.status_code < 300: + logger.info("API ready after %s attempt(s)", attempt) + return + else: + logger.info( + "API not ready (status %s), attempt %s of %s", + r.status_code, + attempt, + retries, + ) + except (httpx.HTTPError, OSError) as e: + logger.info("API not reachable (%s), attempt %s/%s", e, attempt, retries) + + time.sleep(delay) + + raise RuntimeError("API did not become ready in time") diff --git a/docker-compose.local.yml b/docker-compose.local.yml index b2265f39..5bf1efb4 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -1,27 +1,13 @@ services: - db-init: - environment: - STITCH_DB_SEED_MODE: if-needed - api: - environment: - LOG_LEVEL: debug - volumes: - - ./deployments/api/src:/app/deployments/api/src - - ./packages:/app/packages - command: - - uvicorn - - stitch.api.main:app - - --host - - "0.0.0.0" - - --port - - "8000" - - --reload - - --reload-dir - - /app/deployments/api/src - - --reload-dir - - /app/packages - - --reload-exclude - - "*/tests/*" + + adminer: + image: adminer:latest + ports: + - "8081:8080" + profiles: ["tools", "full"] + depends_on: + db: + condition: service_healthy seed: build: @@ -38,10 +24,9 @@ services: SEED_SOURCE: "mixed" NULL_PROBABILITY: 0.3 STATIC_PAYLOAD_DIR: "/mnt/data" + extra_hosts: + - "host.docker.internal:host-gateway" volumes: - ./deployments/seed/data:/mnt/data:ro restart: "no" - depends_on: - api: - condition: service_healthy - + profiles: ["seed", "full"] diff --git a/docker-compose.yml b/docker-compose.yml index f2a3a009..f5257c2b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,5 @@ services: + api: build: context: . @@ -6,14 +7,10 @@ services: env_file: - .env environment: - LOG_LEVEL: info - POSTGRES_DB: stitch - POSTGRES_HOST: db - POSTGRES_PORT: 5432 + LOG_LEVEL: ${API_LOG_LEVEL:-info} # API connects as the app role (no DDL) - STITCH_DB_USER: stitch_app - STITCH_DB_PASSWORD: ${STITCH_APP_PASSWORD:?STITCH_APP_PASSWORD must be set in .env} - AUTH_DISABLED: "true" + POSTGRES_USER: stitch_app + POSTGRES_PASSWORD: ${STITCH_APP_PASSWORD:?STITCH_APP_PASSWORD must be set in .env} healthcheck: test: [ @@ -25,9 +22,9 @@ services: retries: 3 start_period: 5s start_interval: 1s - ports: - "8000:8000" + profiles: ["api", "full"] depends_on: db: condition: service_healthy @@ -50,22 +47,14 @@ services: env_file: - .env environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres + POSTGRES_USER: ${POSTGRES_SUPERUSER:-postgres} + POSTGRES_PASSWORD: ${POSTGRES_SUPERUSER_PASSWORD:-postgres} ports: - "5432:5432" volumes: - db_data:/var/lib/postgresql/data - ./deployments/db/00-init-roles.sh:/docker-entrypoint-initdb.d/00-init-roles.sh:ro - adminer: - image: adminer:latest - ports: - - "8081:8080" - depends_on: - db: - condition: service_healthy - db-init: build: context: . @@ -73,13 +62,10 @@ services: env_file: - .env environment: - STITCH_DB_SCHEMA_MODE: "if-empty" - STITCH_DB_SEED_MODE: "never" - STITCH_DB_SEED_PROFILE: "dev" + LOG_LEVEL: ${DBINIT_LOG_LEVEL:-info} # db-init connects as the migrator role (DDL + seed) - STITCH_DB_USER: stitch_migrator - STITCH_DB_PASSWORD: ${STITCH_MIGRATOR_PASSWORD} - + POSTGRES_USER: stitch_migrator + POSTGRES_PASSWORD: ${STITCH_MIGRATOR_PASSWORD:?STITCH_MIGRATOR_PASSWORD must be set in .env} depends_on: db: condition: service_healthy @@ -91,12 +77,13 @@ services: context: deployments/stitch-frontend dockerfile: Dockerfile args: - VITE_AUTH0_DOMAIN: ${VITE_AUTH0_DOMAIN:-} - VITE_AUTH0_CLIENT_ID: ${VITE_AUTH0_CLIENT_ID:-} - VITE_AUTH0_AUDIENCE: ${VITE_AUTH0_AUDIENCE:-} - VITE_API_URL: ${VITE_API_URL:-http://localhost:8000/api/v1} + VITE_AUTH0_DOMAIN: ${VITE_AUTH0_DOMAIN?} + VITE_AUTH0_CLIENT_ID: ${VITE_AUTH0_CLIENT_ID?} + VITE_AUTH0_AUDIENCE: ${VITE_AUTH0_AUDIENCE?} + VITE_API_URL: ${VITE_API_URL?} ports: - "3000:80" + profiles: ["frontend", "full"] volumes: db_data: diff --git a/env.example b/env.example index a51f749f..4d542f79 100644 --- a/env.example +++ b/env.example @@ -1,17 +1,41 @@ -LOG_LEVEL=info +# Logging +API_LOG_LEVEL=info +DBINIT_LOG_LEVEL=info +SEED_LOG_LEVEL=info -POSTGRES_DB=stitch +# ------------------------------ + +# API +AUTH_DISABLED=true + +# ------------------------------ + +# DB Connection details for API: POSTGRES_HOST=db POSTGRES_PORT=5432 +POSTGRES_DB=stitch +POSTGRES_USER=stitch_app +POSTGRES_PASSWORD=SuperSecureApp1! -STITCH_MIGRATOR_PASSWORD=CHANGE_ME_migrator123! -STITCH_APP_PASSWORD=CHANGE_ME_app123! +# DB bootstrap / roles +POSTGRES_SUPERUSER=postgres +POSTGRES_SUPERUSER_PASSWORD=postgres +STITCH_APP_PASSWORD=SuperSecureApp1! +STITCH_MIGRATOR_PASSWORD=SuperSecureMigrator1! +STITCH_DB_SCHEMA_MODE=if-empty -STITCH_DB_SCHEMA_MODE="if-empty" -STITCH_DB_SEED_MODE="if-needed" -STITCH_DB_SEED_PROFILE="dev" +# ------------------------------ -FRONTEND_ORIGIN_URL=http://localhost:3000 +# Frontend (public) +VITE_API_URL=http://api:8000/api/v1 +VITE_USE_MOCK_DATA=false +VITE_AUTH0_DOMAIN=https://rmi-spd.us.auth0.com +VITE_AUTH0_CLIENT_ID=TS1V1soQbccAV1sitFFCfUaIlSwHD2S2 +VITE_AUTH0_AUDIENCE=https://stitch-api.local -# Auth (AUTH_DISABLED=true bypasses JWT validation for local dev) -AUTH_DISABLED=true + +# ------------------------------ + +# Seed +SEED_API_BASE_URL=http://api:8000/api/v1 +SEED_FAKER_POST_COUNT=5 diff --git a/packages/stitch-ogsi/tests/test_og_field.py b/packages/stitch-ogsi/tests/test_og_field.py index 70f16578..11369d23 100644 --- a/packages/stitch-ogsi/tests/test_og_field.py +++ b/packages/stitch-ogsi/tests/test_og_field.py @@ -61,14 +61,8 @@ class TestOGFieldResource: def test_has_both_base_class_fields(self, og_payload: Sequence[OGFieldSource]): resource = OGFieldResource( id=1, - name="Merged Field", - country="USA", source_data=og_payload, - location_type="Onshore", ) - # OilAndGasFieldBase fields - assert resource.name == "Merged Field" - assert resource.location_type == "Onshore" # Resource fields assert resource.id == 1 assert resource.source_data == og_payload @@ -79,8 +73,6 @@ def test_self_reference_rejected(self, og_payload: Sequence[OGFieldSource]): with pytest.raises(ValidationError, match="constituent of itself"): OGFieldResource( id=1, - name="Bad", - country="USA", source_data=og_payload, constituents=[1], ) diff --git a/scripts/test-package.py b/scripts/test-package.py deleted file mode 100755 index 93d6886b..00000000 --- a/scripts/test-package.py +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env -S uv run --script -# /// script -# requires-python = ">=3.12" -# /// - -import argparse -import subprocess -import sys -import tomllib -from pathlib import Path - - -def find_package_dir(name: str) -> str: - root = tomllib.loads(Path("pyproject.toml").read_text()) - for member in root["tool"]["uv"]["workspace"]["members"]: - p = Path(member) / "pyproject.toml" - if p.exists() and tomllib.loads(p.read_text())["project"]["name"] == name: - return member - print(f"error: package {name!r} not found in workspace", file=sys.stderr) - sys.exit(1) - - -def main() -> None: - parser = argparse.ArgumentParser( - description="Run pytest for a uv workspace package.", - ) - parser.add_argument("package", help="workspace package name") - parser.add_argument( - "-e", - "--exact", - action="store_true", - help="install only the package's declared deps (CI isolation)", - ) - known, pytest_args = parser.parse_known_args() - - pkg_dir = find_package_dir(known.package) - - cmd = ["uv", "run", "--package", known.package, "--active"] - if known.exact: - cmd += ["--exact", "--group", "dev"] - cmd += ["pytest", pkg_dir, *pytest_args] - - sys.exit(subprocess.call(cmd)) - - -main()