Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 25 additions & 17 deletions .github/workflows/api-validation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ jobs:
runs-on: ubuntu-latest

steps:
- name: Checkout repository
- name: Checkout repository (with demo-adapter submodule)
uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ secrets.GITHUB_TOKEN }}

- name: Checkout schema validator repository
Expand All @@ -31,24 +32,37 @@ jobs:
- name: Install uv
run: pip install uv

- name: Build an image
- name: Build the main image
run: docker build --platform=linux/amd64 -t iri-facility-api-base .

- name: Run Facility API container
# Mount the demo adapter from the examples/demo-adapter submodule so schemathesis runs
# against the PR's main code backed by the demo data.
- name: Run Facility API container (main image + demo adapter submodule)
run: |
docker run -d \
-p 8000:8000 \
--platform=linux/amd64 \
--name iri-facility-api-base \
-e IRI_API_ADAPTER_facility=app.demo_adapter.DemoAdapter \
-e IRI_API_ADAPTER_status=app.demo_adapter.DemoAdapter \
-e IRI_API_ADAPTER_account=app.demo_adapter.DemoAdapter \
-e IRI_API_ADAPTER_compute=app.demo_adapter.DemoAdapter \
-e IRI_API_ADAPTER_filesystem=app.demo_adapter.DemoAdapter \
-e IRI_API_ADAPTER_task=app.demo_adapter.DemoAdapter \
-v "$GITHUB_WORKSPACE/examples/demo-adapter:/demo-adapter" \
-e PYTHONPATH=/demo-adapter \
-e IRI_API_ADAPTER_facility=demo_adapter.combined.DemoAdapter \
-e IRI_API_ADAPTER_status=demo_adapter.combined.DemoAdapter \
-e IRI_API_ADAPTER_account=demo_adapter.combined.DemoAdapter \
-e IRI_API_ADAPTER_compute=demo_adapter.combined.DemoAdapter \
-e IRI_API_ADAPTER_filesystem=demo_adapter.combined.DemoAdapter \
-e IRI_API_ADAPTER_storage=demo_adapter.combined.DemoAdapter \
-e IRI_API_ADAPTER_task=demo_adapter.combined.DemoAdapter \
-e IRI_IDEMPOTENCY_STORE=demo_adapter.compute.idempotency.InMemoryIdempotencyStore \
-e API_URL_ROOT=http://127.0.0.1:8000 \
-e IRI_API_TOKEN=12345 \
iri-facility-api-base
iri-facility-api-base \
sh -c "uv pip install --system 'redis>=7.2.0,<8.0.0' && fastapi run app/main.py --port 8000"

- name: Create venv & install validator dependencies
run: |
uv venv
source .venv/bin/activate
uv pip install -r schema-validator/verification/requirements.txt

- name: Wait for API to be ready
run: |
Expand All @@ -62,16 +76,10 @@ jobs:
echo "API did not start"
exit 1

- name: Create venv & install validator dependencies
run: |
uv venv
source .venv/bin/activate
uv pip install -r schema-validator/verification/requirements.txt

- name: Run Schemathesis validation (local spec)
id: schemathesis_local
env:
IRI_API_TOKEN: "12345" # This is dummy token for testing (mock adapter)
IRI_API_TOKEN: "12345" # This is dummy token for testing (demo adapter)
run: |
set +e
source .venv/bin/activate
Expand Down
4 changes: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[submodule "examples/demo-adapter"]
path = examples/demo-adapter
url = git@github.com:doe-iri/iri-facility-api-demo-adapter.git
branch = syncmaintodemo
18 changes: 11 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,20 +26,24 @@ $(STAMP_DEPS): $(STAMP_VENV) pyproject.toml
pylint \
bandit \
pytest
git submodule update --init examples/demo-adapter
$(UV) pip install --python $(BIN)/python -e examples/demo-adapter --no-deps
$(UV) pip install --python $(BIN)/python 'redis>=7.2.0,<8.0.0'
touch $(STAMP_DEPS)

deps: $(STAMP_DEPS)

dev: deps
@source $(BIN)/activate && \
[ -f local.env ] && source local.env || true && \
IRI_API_ADAPTER_facility=app.demo_adapter.DemoAdapter \
IRI_API_ADAPTER_status=app.demo_adapter.DemoAdapter \
IRI_API_ADAPTER_account=app.demo_adapter.DemoAdapter \
IRI_API_ADAPTER_compute=app.demo_adapter.DemoAdapter \
IRI_API_ADAPTER_filesystem=app.demo_adapter.DemoAdapter \
IRI_API_ADAPTER_storage=app.demo_adapter.DemoAdapter \
IRI_API_ADAPTER_task=app.demo_adapter.DemoAdapter \
IRI_API_ADAPTER_facility=demo_adapter.combined.DemoAdapter \
IRI_API_ADAPTER_status=demo_adapter.combined.DemoAdapter \
IRI_API_ADAPTER_account=demo_adapter.combined.DemoAdapter \
IRI_API_ADAPTER_compute=demo_adapter.combined.DemoAdapter \
IRI_API_ADAPTER_filesystem=demo_adapter.combined.DemoAdapter \
IRI_API_ADAPTER_storage=demo_adapter.combined.DemoAdapter \
IRI_API_ADAPTER_task=demo_adapter.combined.DemoAdapter \
IRI_IDEMPOTENCY_STORE=demo_adapter.compute.idempotency.InMemoryIdempotencyStore \
IRI_LOG_FILE="$${IRI_LOG_FILE:-$${LOG_FILE:-$(IRI_LOG_FILE)}}" \
IRI_LOG_ROTATION_DAYS="$${IRI_LOG_ROTATION_DAYS:-$${LOG_ROTATION_DAYS:-$(IRI_LOG_ROTATION_DAYS)}}" \
DEMO_QUEUE_UPDATE_SECS=2 \
Expand Down
23 changes: 15 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ This behavior is specific to compute submission/update handling; read-only endpo

The specific implementations can be specified via the `IRI_API_ADAPTER_*` environment variables. For example the adapter for the `status` api would be given by setting `IRI_API_ADAPTER_status` to the full python module and class implementing `app.routers.status.facility_adapter.FacilityAdapter`. (eg. `IRI_API_ADAPTER_status=myfacility.MyFacilityStatusAdapter`)

As a default implementation, this project supplies the [demo adapter](app/demo_adapter.py) which implements every facility adapter with fake data.
A reference implementation that fakes every facility adapter is provided by the separate [`iri-facility-api-demo-adapter`](https://github.com/doe-iri/iri-facility-api-demo-adapter) repo, included here as a git submodule under `examples/demo-adapter`. `make dev` installs it and wires it up automatically. This repo itself ships no adapter -- it is a pure framework.

### Customizing the API meta-data
You can optionally override the [FastAPI metadata](https://fastapi.tiangolo.com/tutorial/metadata/), such as `name`, `description`, `terms_of_service`, etc. by providing a valid json object in the `IRI_API_PARAMS` environment variable.
Expand Down Expand Up @@ -120,9 +120,9 @@ Links to data, created by this api, will concatenate these values producing link
| `IRI_API_ADAPTER_storage` | `/storage/...` | [`app.routers.storage.facility_adapter.FacilityAdapter`](app/routers/storage/facility_adapter.py) |
| `IRI_API_ADAPTER_task` | `/task/...` | [`app.routers.task.facility_adapter.FacilityAdapter`](app/routers/task/facility_adapter.py) |

Each value is a `module.path.ClassName` string. `app.demo_adapter.DemoAdapter` implements all of them and is what `make dev` wires up by default. A router whose `IRI_API_ADAPTER_*` is not set is hidden from the API at startup unless `IRI_SHOW_MISSING_ROUTES=true`.
Each value is a `module.path.ClassName` string. The demo adapter's `demo_adapter.combined.DemoAdapter` (from the `examples/demo-adapter` submodule) implements all of them and is what `make dev` wires up by default. A router whose `IRI_API_ADAPTER_*` is not set is hidden from the API at startup; if `IRI_SHOW_MISSING_ROUTES=true` an unconfigured router instead fails fast at startup (the framework has no built-in fallback adapter).

- `IRI_SHOW_MISSING_ROUTES`: hide api groups that don't have an `IRI_API_ADAPTER_*` environment variable defined, if set to `true`. This way if your facility only wishes to expose some api groups but not others, they can be hidden. (Defaults to `false`.)
- `IRI_SHOW_MISSING_ROUTES`: by default (`false`), api groups without an `IRI_API_ADAPTER_*` environment variable are silently hidden, so a facility can expose only the groups it implements. If set to `true`, an unconfigured group instead makes startup fail fast, surfacing the missing adapter as a configuration error rather than silently dropping the route.

### Logging

Expand Down Expand Up @@ -154,18 +154,24 @@ Compute `submit_job` and `update_job` endpoints support an optional `Idempotency

### Backing store

| `REDIS_URL` set? | Store used | Suitable for |
The core library ships no backing store. Configure one with `IRI_IDEMPOTENCY_STORE`;
if it is unset, a request that sends `Idempotency-Key` returns `501`.

The demo adapter package provides reference stores:

| Store | Configure with | Suitable for |
|---|---|---|
| No (default) | In-process dict | Dev / single-instance |
| Yes | Redis | Multi-replica production |
| In-process dict | `IRI_IDEMPOTENCY_STORE=demo_adapter.compute.idempotency.InMemoryIdempotencyStore` | Dev / single-instance |
| Redis | `IRI_IDEMPOTENCY_STORE=demo_adapter.compute.idempotency.RedisIdempotencyStore` plus `REDIS_URL` | Multi-replica production |

For multi-replica deployments, Redis is required. Run a local Redis instance with `make redis`.
For multi-replica deployments, use the Redis store. Run a local Redis instance with `make redis`.

### Environment variables

| Variable | Default | Description |
|---|---|---|
| `REDIS_URL` | _(unset)_ | Redis connection URL (e.g. `redis://localhost:6379`). When unset, uses in-memory store. |
| `IRI_IDEMPOTENCY_STORE` | _(unset)_ | Dotted path to an idempotency store class. The demo adapter provides in-memory and Redis reference stores. |
| `REDIS_URL` | _(unset)_ | Redis connection URL (e.g. `redis://localhost:6379`) when using `RedisIdempotencyStore`. |
| `IDEMPOTENCY_TTL_SECONDS` | `86400` | How long a cached response is retained after a successful call (24 hours). |
| `LOCK_TTL_SECONDS` | `60` | Maximum seconds an in-flight request holds the lock. If the IRI process crashes mid-request, the lock auto-expires after this interval so the next retry is treated as a fresh request. Set higher if your facility's scheduler API is known to be slow. |

Expand All @@ -174,6 +180,7 @@ For multi-replica deployments, Redis is required. Run a local Redis instance wit
```bash
make redis # start Redis container on :6379
# add to local.env:
export IRI_IDEMPOTENCY_STORE=demo_adapter.compute.idempotency.RedisIdempotencyStore
export REDIS_URL=redis://localhost:6379
make # start IRI dev server
```
Expand Down
5 changes: 3 additions & 2 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@

# Idempotency store
# IRI_IDEMPOTENCY_STORE: fully-qualified class to use as the backing store.
# Example: IRI_IDEMPOTENCY_STORE=app.demo_adapter.DemoRedisIdempotencyStore
# Example: IRI_IDEMPOTENCY_STORE=demo_adapter.compute.idempotency.RedisIdempotencyStore
# If unset, idempotency is disabled (an Idempotency-Key request returns 501).
IRI_IDEMPOTENCY_STORE = os.environ.get("IRI_IDEMPOTENCY_STORE", "")
IDEMPOTENCY_TTL_SECONDS = int(os.environ.get("IDEMPOTENCY_TTL_SECONDS", "86400"))
LOCK_TTL_SECONDS = int(os.environ.get("LOCK_TTL_SECONDS", "60"))
Expand All @@ -72,7 +73,7 @@
logger.info(f"OTEL_TRACES_ENABLED={OTEL_TRACES_ENABLED}")
logger.info(f"OTEL_METRICS_ENABLED={OTEL_METRICS_ENABLED}")
logger.info(f"OTEL_METRIC_EXPORT_INTERVAL={OTEL_METRIC_EXPORT_INTERVAL}")
logger.info(f"IRI_IDEMPOTENCY_STORE={IRI_IDEMPOTENCY_STORE if IRI_IDEMPOTENCY_STORE else '(unset, using in-memory store)'}")
logger.info(f"IRI_IDEMPOTENCY_STORE={IRI_IDEMPOTENCY_STORE if IRI_IDEMPOTENCY_STORE else '(unset, idempotency disabled)'}")
logger.info(f"IDEMPOTENCY_TTL_SECONDS={IDEMPOTENCY_TTL_SECONDS}")
logger.info(f"LOCK_TTL_SECONDS={LOCK_TTL_SECONDS}")
logger.info("="*40)
Loading
Loading