Mock Server for Local Development¶
The mock server is a local test server that mimics the DeepOrigin Platform API. It's useful for local development and testing without making real API calls to the platform.
Overview¶
The mock server (tests/mock_server/) provides mock responses for all API endpoints used by the DeepOriginClient. It runs locally using FastAPI and uvicorn, and serves responses based on fixture data stored in tests/fixtures/.
Architecture¶
The mock server is organized into routers, each handling a group of related endpoints:
| Router | File | Responsibilities |
|---|---|---|
| data_platform | routers/data_platform.py |
Entity CRUD (proteins, ligands), entity search (offset or cursor, with meta.nextCursor while more rows remain), result-explorer search |
| tools | routers/tools.py |
Tool executions (list/get/cancel/confirm/run), clusters |
| files | routers/files.py |
File upload/download |
| entities | routers/entities.py |
Entity management |
| billing | routers/billing.py |
Billing endpoints |
| sse | routers/sse.py |
Gateway project event stream (/sse/{org}/stream/{project}) |
All routers share in-memory stores (dicts/lists) that are created in MockServer.__init__ and passed into the router factory functions. This lets data flow between routers — for example, a tool execution in the tools router can inject records that are later visible via the data-platform router's result-explorer search.
Proteins (local only): Protein.sync() is wired through mock import-dataset
register_protein to a single canonical row (MOCK_CANONICAL_PROTEIN_ID,
tests/brd.pdb fixture) so IDs stay stable under --env local. There is no
separate test module for the mock server; that behavior is exercised indirectly
by any local test that syncs a protein (e.g. the registered_protein fixture).
SSE stream (local only): GET /sse/{org}/stream/{project} sends a retry:
line, one filters.dirty frame naming every filter=<id>=<pattern> query
param, then closes. Executions.wait_for_ingestion reads the execution row on
connect, re-reads on that frame, and reconnects only if the row is still not
terminal, so a row that reads DataIngesting once (see executions/search)
resolves on the second read. While there is no row, the wait also reads the
tools-service run and ends if the run failed. The stream for project
MOCK_SSE_DISABLED_PROJECT_ID (tests/mock_server/constants.py) is refused
with 404, as on a gateway without SSE, to test that the wait warns and polls the
row instead.
Protein import (local only): an import-dataset csv_path run whose mapper targets proteins.*, as projects.sync_proteins sends, is ingested at submit (_ingest_import_dataset_protein_csv in routers/tools.py). Rows are read through the mapper's proteins.<field> keys, with header keys and cells stripped as the import tool does. A CSV with no rows ends the run Failed with a statusReason.
Protein zip (local only): with protein_zip_path, a row's file_path names a file in the uploaded zip, mirroring import-dataset 4.x (_protein_zip_entries and _register_zip_protein in routers/tools.py):
- Zip entries are matched by file name at any depth; the first match wins.
- Registrations are cached by that file name, so rows naming the same file share one protein. This is why
build_protein_bundlegives every entry a unique internal name. - Each file is stored at
entities/proteins/<sha256><ext>. A project protein with the same content is reused and keeps its name. The mock hashes file bytes where the tool hashes coordinates. - A missing file, or one without
ATOM/HETATMlines, loses itsfile_pathsilently. The row is still imported as a protein with only its name, and the run endsCompleted, as the tool does on dev.
Covered by tests/test_protein_folder_import_local.py.
Pose registration (local only): PoseSet.sync() runs a synchronous
import-dataset process_sdf with register_poses, waits on
client.executions.wait_for_ingestion, then reads the run's pose rows from
result-explorer once. The mock covers each step:
- The import mirrors its
jobOutputs["poses"]into result-explorer, each row with anid, the run'scompute_job_idand the SDFrecord_index. The run's data-platform row readsDataIngestingon the firstexecutions/searchandCompletedafter that. - An SDF record named
MOCK_POSE_REGISTRATION_FAIL_NAME(routers/tools.py) makes the run endFailedwithstatusReasonMOCK_POSE_REGISTRATION_FAIL_REASONand no outputs.executions/searchreturns no row for it, as when a run fails before data platform records it. - An SDF record named
MOCK_POSE_INGESTION_FAIL_NAMElets the run complete while its data-platform row readsFailed. - An SDF record named
MOCK_POSE_REJECTED_NAMEstays injobOutputsbut is left out of result-explorer while the run completes. This mimics the platform rejecting one record during storage and drives the short-count error. - A synchronous import-dataset run whose inputs include
result_laneis rejected with HTTP 400 unless the requested tool version is at leastIMPORT_DATASET_RESULT_LANE_MIN_VERSION(src/utils/constants.py). This mirrors older import-dataset input schemas, which reject unknown keys. Pose registration sendsresult_laneonly when the pinned version accepts it.
Running the Mock Server¶
Using in Tests¶
Pass --env local to pytest:
uv run pytest --env local
This starts a MockServer on port 4931 for the duration of the test session (see conftest.py). With DO_ENV=local, DeepOriginClient defaults to http://127.0.0.1:4931 (same as the mock).
Full platform locally (gateway on 6010): set DO_BASE_URL=http://127.0.0.1:6010 (and keep or set DO_ENV=local if inferred from URL). See LOCAL_ENDPOINT_GATEWAY in src/utils/constants.py.
The same tests can also run against a real environment:
uv run pytest --env dev
When --env dev is used, the mock server is not started and all requests go to the real platform API. This dual-mode design means the mock server must produce responses that are structurally identical to the real API — it is not a shortcut that skips validation.
Live test project: level-1 and integration tests against dev/staging/prod resolve a shared project by display name (do-dd-client-tests, see tests/integration_project.py). The canonical id is fetched from the platform at session start (create-if-missing via projects.create); it is not hard-coded. Set DO_PROJECT_ID to pin a different project without renaming. Local tests keep using the mock-only id 09DEFAULTPROJECT00.
Standalone Script¶
To run the mock server standalone for local development:
python tests/run_mock_server.py [PORT]
Where PORT is the port number to run the server on (default: 4931 via make mock-server).
Optional flags:
uv run python -m tests.run_mock_server --port 4931 --abfe-duration 30 --rbfe-duration 30
RBFE progress tree (local)¶
While an RBFE execution is Running, the mock synthesizes a v2 progress tree
(matching dev's progressReport shape) instead of the legacy {"complete": N}
bar. Stages follow platform-toolbox/tools/rbfe/workflow/workflow.yaml:
prepare-inputs→run-konnektor(Skipped when absent fromsteps) →build-pair-list→pair-pipelineper pair- Inside each pair:
system-prep-task(Skipped forsteps=["rbfe"]) →resolve-prepared-system→rbfe-e2e-task
Children pop up as the mock run advances. The simulation leaf ramps
toolProgress.complete from 0 to 100 (not the legacy top-level complete key).
Pace the run with --rbfe-duration on run_mock_server (default 30s for
standalone make mock-server; pytest --env local keeps 5s for speed).
Implementation: build_rbfe_progress_tree() and _get_rbfe_progress_report() in
tests/mock_server/routers/tools.py.
RBFE notebook (local)¶
The working RBFE notebook is
docs/notebooks/clean/rbfe-1-single-pair.ipynb.
It runs against the mock when
DO_ENV=local and the server is up:
make mock-server # port 4931
Set DO_ENV=local in .env (and JOB_WATCH_BLOCK=1 for nbconvert --execute), then open the notebook. Fixtures are under:
| Path | Purpose |
|---|---|
tests/fixtures/executions/a5484958-059f-4b1b-ba2c-664adf23e8e8.json |
Preloaded succeeded RBFE execution (RBFE.from_id) |
tests/fixtures/result-explorer-rbfe-a5484958.json |
Result-explorer rows for that job |
tests/fixtures/user_logs-rbfe-a5484958.json |
User log rows injected after RBFE success |
tests/fixtures/files/tool-runs/a5484958-.../workflow-mock/binding_nohup.out |
Workflow log served at tool-runs/{executionId}/workflow-mock/ |
tests/fixtures/deeporigin.rbfe/quotation-result.json |
Quote response for deeporigin.rbfe |
tests/fixtures/tool-runs/deeporigin.rbfe/result-template.json |
Injected ΔΔG row after confirm/watch |
tests/fixtures/tool-runs/deeporigin.system-prep/run-rbfe.json |
RBFE-mode system-prep (ligand1 + ligand2) |
Integration coverage: tests/test_rbfe_local.py (uv run pytest tests/test_rbfe_local.py --env local).
How Tool Executions Work¶
This is the most complex part of the mock server. When client code runs a tool (e.g., Docking(protein=..., pocket=..., ligand=...).run(), PocketFinder(protein).run(), or Molprops(...).run()), the following happens end-to-end. All runnable work goes through client.executions.create() and the mock's POST /tools/{org}/tools/{tool_key}/{tool_version}/executions route (run_tool in routers/tools.py).
1. Building the execution DTO¶
run_tool inspects the request body and the URL's tool_key and produces a tool execution DTO with executionId, status, userInputs, jobOutputs, quotationResult, tool: {key, version}, and type: "ToolExecution". Several tool keys are special-cased:
deeporigin.docking(single ligand,sync=True),deeporigin.pocket-finder,deeporigin.system-prep,deeporigin.protein-prepcomplete in one POST via_create_blocking_run_dtoand inject their fixture-backedjobOutputsinto the result-explorer pool. Protein Prep v2 branches oninputs.action:recommendloadstool-runs/deeporigin.protein-prep/recommend.json;prepare(or omitted action) loadsrun.json.deeporigin.mol-props-protonationreturns deterministicprotonation_statesfrom the request inputs (_build_protonation_execution).deeporigin.mol-props-*(logd, logp, ames, …) hash the normalized request body and look up the recorded outputs attests/fixtures/tool-runs/{tool_key}/{body_hash}.json(_build_molprops_execution). The fixture's per-ligandjobOutputsrows are re-keyed by the request's ligand IDs.deeporigin.mol-props-combined(Molprops) synthesizes per-ligand rows and a matchingquotationResult(_build_combined_molprops_execution). When the request body includesapproveAmount: -1(quote-only;<= 0also accepted), the mock clearsjobOutputsand setsstatustoQuoted.deeporigin.bulk-docking(approveAmount<=0) loadstests/fixtures/tool-runs/deeporigin.bulk-docking/quote.jsonand scales the quotation by ligand count.- All other tool keys fall back to a generic
Quotedexecution DTO via_create_execution_dto.
2. Request hashing for molprops fixtures¶
For deeporigin.mol-props-*, _build_molprops_execution normalizes the request body with normalize_tool_execution_body (strips IDs, clusterId, tag, app, session) and hashes it to find a fixture:
tests/fixtures/tool-runs/{tool_key}/{body_hash}.json
Because IDs are stripped before hashing, the same fixture handles different ligand IDs; the per-row ligand_id is patched in afterwards from the request's ligand inputs.
3. ID replacement in tool outputs¶
Some fixtures (docking, pocket-finder, system-prep) were recorded with whatever protein/ligand IDs existed at recording time, but in tests entities get fresh IDs every run. _replace_ids_in_outputs recursively walks the fixture's jobOutputs (or the legacy functionOutputs field on older fixtures) and rewrites any protein_id / ligand_id / ligand1_id value to match the IDs from the current request's userInputs.
4. Result-explorer injection¶
In production, when a tool execution completes, a message-queue flow writes structured outputs (pockets, poses, system, …) into a result-explorer table. Client code later queries this table to retrieve results — for example, Docking.get_results() calls client.results.get_poses(compute_job_id=...), which searches the result-explorer.
The mock server emulates this with _inject_result_explorer_records_from_outputs. After a tool execution returns, it checks the output_key_map:
output_key_map = {
"deeporigin.pocketfinder": ("pockets", "pocket"),
"deeporigin.pocket-finder": ("pockets", "pocket"),
"deeporigin.docking": ("poses", "pose"),
"deeporigin.system-prep": ("system", "preparedsystem"),
"deeporigin.protein-prep": ("protein", "preparedprotein"),
}
Each entry maps a tool manifest key to the jobOutputs field that should be mirrored into the shared result-explorer store and the result_type to tag those records with. Tool-specific helpers (_inject_docking_tool_execution_results, _inject_pocketfinder_tool_execution_results, _inject_sysprep_tool_execution_results, _inject_protein_prep_tool_execution_results) load the same fixture shapes and call this generic injector.
When adding a new tool, extend output_key_map and any tool-specific injectors in routers/tools.py so downstream queries (e.g., Pocket.from_result, Docking.get_results()) can find the records.
5. Result-explorer queries¶
When client code queries results (e.g., client.results.get_poses(compute_job_id=...)), it hits the data-platform router's result-explorer/search endpoint. This applies _apply_eq_filters, which checks filter values against both top-level record fields and nested data fields. Since the injected records store tool outputs under data, a filter like protein_id: {eq: "abc123"} matches record["data"]["protein_id"].
End-to-end example: docking¶
Here's the full flow for test_docking_run_lv2 in tests/test_docking.py:
1. registered_protein fixture
→ POST /data-platform/{org}/proteins (creates protein, gets fresh ID)
2. registered_ligand fixture
→ POST /data-platform/{org}/ligands (creates or syncs ligand)
3. Docking(protein=..., pocket=..., ligand=...).run()
→ POST /tools/{org}/tools/deeporigin.docking/{version}/executions
→ run_tool:
a. build a Completed execution DTO via _create_blocking_run_dto
b. _inject_docking_tool_execution_results loads the docking fixture,
replaces protein_id/ligand_id, and pushes the "poses" rows into
the result-explorer store
4. run() → get_results() → client.results.get_poses(compute_job_id=...)
→ POST /data-platform/{org}/result-explorer/search
filter: {compute_job_id: {eq: "<executionId>"}, ...}
→ _apply_eq_filters matches the injected rows
→ returns the poses injected in step 3b (falls back to jobOutputs.poses)
Entity Stores and Fixtures¶
In-memory stores¶
The mock server maintains in-memory dicts for proteins and ligands. These start empty for proteins (fresh registration every test run) and pre-populated for ligands (from SDF files and JSON fixtures).
Ligand pre-population¶
_load_ligand_fixtures scans multiple sources on startup:
src/data/brd/*.sdf— package-data ligandstests/fixtures/ligands-brd-all.sdf,42-ligands.sdf,brd-7.sdftests/fixtures/ligand_*.json
Ligand IDs are deterministic — derived from a SHA-256 hash of the canonical SMILES. This means the same ligand always gets the same ID across test runs, which is important for fixture matching.
Result-explorer pre-population¶
_load_result_explorer_fixtures loads any tests/fixtures/result-explorer-*.json files into the shared results store at startup. Tool executions then append to this same store at runtime.
Configuring Your Client¶
To use the mock server with your code, configure the DeepOriginClient to point to the mock server URL:
from deeporigin.platform.client import DeepOriginClient
client = DeepOriginClient(
token="test-token", # Any token works with the mock server
org_key="deeporigin", # Use any org_key
base_url="http://127.0.0.1:4931", # Mock server URL (port 4931 for tests)
env="local",
)
Available Endpoints¶
The mock server implements the following endpoints:
- Files API: List, upload, download, and delete files. Listing a folder that isn't in
tests/fixtures/filesreturns the files uploaded under it during the session (e.g.imports/staging). - Tools API: List tools and tool definitions
- Executions API: List, get, cancel, confirm, and create tool executions
- Clusters API: List available clusters
- Entities API: Entity management (delete)
- Data Platform API: Entity CRUD (proteins, ligands), result-explorer search
- Billing API: Billing endpoints
- SSE stream: Gateway project stream that marks requested filters dirty once, then closes
- Health Check:
/healthendpoint
Extending the Mock Server¶
Adding a new tool¶
- Record a fixture by running the tool against
--env devand saving the response astests/fixtures/tool-runs/{tool_key}/{body_hash}.json(orrun.json/quote.jsonfor tools that don't hash by request body). - If the tool produces structured outputs that need to be queryable via result-explorer, add an entry to
output_key_mapin_inject_result_explorer_records_from_outputs(tests/mock_server/routers/tools.py) and a small injector helper for that tool. - Run the test with
--env localto verify.
Adding a new entity endpoint¶
- Add a route handler to the appropriate router in
tests/mock_server/routers/ - Optionally add fixture files in
tests/fixtures/if you need realistic data - Use
load_fixture()to load JSON fixtures
Adding a new router¶
- Create a new file in
tests/mock_server/routers/ - Define a
create_*_router()factory function that accepts shared state - Include the router in
MockServer._setup_routes()inserver.py
Limitations¶
- Authentication is not validated (any token works)
- File storage is in-memory and lost when the server stops
- Tool executions return pre-recorded fixtures, not computed results
- Rate limiting and other production features are not implemented
For production use, always use the real DeepOrigin Platform API.