Serve documentation from the operator UI
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from scimesh.cli import main
|
||||
|
||||
|
||||
def test_help_command_prints_runnable_examples(capsys: object) -> None:
|
||||
def test_help_command_prints_runnable_examples(
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
assert main(["help"]) == 0
|
||||
output = capsys.readouterr().out
|
||||
assert "scimesh similarity-search" in output
|
||||
|
||||
@@ -4,10 +4,14 @@ from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from scimesh.cli import main
|
||||
|
||||
|
||||
def test_workload_cli_lists_sdk_workloads(capsys: object) -> None:
|
||||
def test_workload_cli_lists_sdk_workloads(
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
assert main(["workload", "list"]) == 0
|
||||
output = capsys.readouterr().out
|
||||
assert "descriptor-batch" in output
|
||||
@@ -16,7 +20,9 @@ def test_workload_cli_lists_sdk_workloads(capsys: object) -> None:
|
||||
assert "enabled" in output
|
||||
|
||||
|
||||
def test_workload_cli_runs_descriptor_batch(tmp_path: Path, capsys: object) -> None:
|
||||
def test_workload_cli_runs_descriptor_batch(
|
||||
tmp_path: Path, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
dataset = tmp_path / "molecules.tsv"
|
||||
dataset.write_text(
|
||||
"chembl_id\tcanonical_smiles\nA\tCCO\nB\tCCCC\nC\tCCN\n",
|
||||
@@ -107,7 +113,7 @@ def test_workload_cli_rejects_invalid_params_json(tmp_path: Path) -> None:
|
||||
|
||||
|
||||
def test_workload_cli_runs_an_allowlisted_custom_workload(
|
||||
tmp_path: Path, monkeypatch: object, capsys: object
|
||||
tmp_path: Path, monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
||||
) -> None:
|
||||
import csv
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import pytest
|
||||
from scimesh.chemistry.dataset import find_molecule_by_id
|
||||
from scimesh.sdk import (
|
||||
ArtifactCollection,
|
||||
WorkloadManifest,
|
||||
ArtifactSchema,
|
||||
CheckpointPolicy,
|
||||
CompatibilityError,
|
||||
@@ -498,7 +499,7 @@ def test_local_executor_rejects_non_trusted_trust_modes(tmp_path: Path) -> None:
|
||||
def _advanced_execution_manifest(
|
||||
original: WorkloadDefinition,
|
||||
case: str,
|
||||
) -> tuple[tuple[str, ...], object]:
|
||||
) -> tuple[tuple[str, ...], WorkloadManifest]:
|
||||
"""Declare one negotiable advanced profile the local executor cannot enforce."""
|
||||
stages = original.manifest.workflow.stages
|
||||
if case == "process-pool":
|
||||
|
||||
@@ -250,9 +250,9 @@ def test_json_backed_values_are_deeply_immutable_and_detached_from_callers() ->
|
||||
with pytest.raises(TypeError):
|
||||
manifest.environment.metadata["python"]["version"][0] = 2
|
||||
with pytest.raises(TypeError):
|
||||
manifest.inputs["another"] = manifest.inputs["dataset"]
|
||||
manifest.inputs["another"] = manifest.inputs["dataset"] # type: ignore[index]
|
||||
with pytest.raises(FrozenInstanceError):
|
||||
manifest.description = "changed"
|
||||
manifest.description = "changed" # type: ignore[misc]
|
||||
|
||||
|
||||
def test_collection_kinds_have_distinct_ordering_key_and_duplicate_semantics() -> None:
|
||||
@@ -324,7 +324,7 @@ def test_workflow_graph_validation_fails_closed_for_unbound_or_inconsistent_depe
|
||||
WorkflowSpec.from_dict(payload)
|
||||
|
||||
payload = workload_manifest().workflow.to_dict()
|
||||
payload["stages"][0]["needs"] = ["undeclared-stage"]
|
||||
payload["stages"][0]["needs"] = ["undeclared-stage"] # type: ignore[index]
|
||||
with pytest.raises(ValueError, match="needs do not match"):
|
||||
WorkflowSpec.from_dict(payload)
|
||||
|
||||
@@ -549,14 +549,14 @@ def test_expansion_is_bound_to_coordinator_parent_and_remaining_budget() -> None
|
||||
source = ArtifactCollection.single(artifact("dynamic-source"))
|
||||
planned = ArtifactCollection.single(artifact("dynamic-planned"))
|
||||
parent = TaskSpec(
|
||||
**common,
|
||||
**common, # type: ignore[arg-type]
|
||||
task_key="root/planner",
|
||||
stage_id="planner",
|
||||
inputs={"dataset": source},
|
||||
expected_outputs={"planned": port},
|
||||
)
|
||||
child = TaskSpec(
|
||||
**common,
|
||||
**common, # type: ignore[arg-type]
|
||||
task_key="root/planner/000",
|
||||
stage_id="compute",
|
||||
inputs={"dataset": planned},
|
||||
@@ -992,7 +992,7 @@ def test_allocation_environment_exposes_only_allocation_derived_values() -> None
|
||||
"ROCR_VISIBLE_DEVICES": "GPU-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
|
||||
}
|
||||
with pytest.raises(TypeError):
|
||||
environment["OMP_NUM_THREADS"] = "1"
|
||||
environment["OMP_NUM_THREADS"] = "1" # type: ignore[index]
|
||||
|
||||
cpu_only = replace(allocation, accelerator_ids=())
|
||||
assert profile.allocation_environment(cpu_only)["CUDA_VISIBLE_DEVICES"] == ""
|
||||
|
||||
@@ -87,6 +87,7 @@ def test_compatibility_failure_occurs_before_planner_invocation(
|
||||
|
||||
class CountingPlanner:
|
||||
calls = 0
|
||||
entry_point = "tests.sdk_fixture:plan@v1"
|
||||
|
||||
def validate(self, request):
|
||||
self.calls += 1
|
||||
|
||||
@@ -92,7 +92,7 @@ def test_resource_inventory_and_requirements_round_trip_without_mutable_aliases(
|
||||
capabilities["compute"] = "mutated"
|
||||
assert device.capabilities["compute"] == "9.0"
|
||||
with pytest.raises(TypeError):
|
||||
device.capabilities["compute"] = "mutated"
|
||||
device.capabilities["compute"] = "mutated" # type: ignore[index]
|
||||
assert ResourceInventory.from_dict(inventory.to_dict()) == inventory
|
||||
assert ResourceRequirements.from_dict(requirements.to_dict()) == requirements
|
||||
assert requirements.eligibility_errors(inventory) == ()
|
||||
|
||||
@@ -523,7 +523,7 @@ def test_exact_verifier_rejects_owner_equivocation_without_leaking_identity() ->
|
||||
"candidate_count": 3,
|
||||
"equivocating_owner_count": 1,
|
||||
}
|
||||
assert candidates.candidates[0].owner_id not in json.dumps(decision.to_dict())
|
||||
assert (candidates.candidates[0].owner_id or "") not in json.dumps(decision.to_dict())
|
||||
|
||||
|
||||
def test_numeric_verifier_accepts_nested_values_with_absolute_and_relative_tolerance() -> None:
|
||||
|
||||
+39
-23
@@ -49,7 +49,9 @@ class SeqOpener:
|
||||
|
||||
|
||||
def _exchange_response(token: str, expires_in: int) -> FakeResponse:
|
||||
return FakeResponse(200, json.dumps({"token": token, "expires_in": expires_in}).encode())
|
||||
return FakeResponse(
|
||||
200, json.dumps({"token": token, "expires_in": expires_in}).encode()
|
||||
)
|
||||
|
||||
|
||||
def test_static_provider_returns_fixed_token_and_never_refreshes():
|
||||
@@ -68,13 +70,13 @@ def test_worker_key_provider_exchanges_once_then_caches():
|
||||
provider = WorkerKeyTokenProvider(
|
||||
"http://users", "scimesh_wk_live_x", timeout=5, now=lambda: clock["t"]
|
||||
)
|
||||
provider._opener = SeqOpener([_exchange_response("jwt-1", 100)])
|
||||
provider._opener = SeqOpener([_exchange_response("jwt-1", 100)]) # type: ignore[reportAttributeAccessIssue]
|
||||
|
||||
# First call exchanges; a second call well within the TTL reuses the cache.
|
||||
assert provider.token() == "jwt-1"
|
||||
clock["t"] = 1050.0 # 50s later, TTL 100s with 0.2 leeway → refresh at +80s
|
||||
assert provider.token() == "jwt-1"
|
||||
assert len(provider._opener.requests) == 1
|
||||
assert len(provider._opener.requests) == 1 # type: ignore[reportAttributeAccessIssue]
|
||||
|
||||
|
||||
def test_worker_key_provider_refreshes_after_leeway():
|
||||
@@ -82,22 +84,26 @@ def test_worker_key_provider_refreshes_after_leeway():
|
||||
provider = WorkerKeyTokenProvider(
|
||||
"http://users", "k", timeout=5, now=lambda: clock["t"]
|
||||
)
|
||||
provider._opener = SeqOpener([
|
||||
_exchange_response("jwt-1", 100),
|
||||
_exchange_response("jwt-2", 100),
|
||||
])
|
||||
provider._opener = SeqOpener( # type: ignore[reportAttributeAccessIssue]
|
||||
[
|
||||
_exchange_response("jwt-1", 100),
|
||||
_exchange_response("jwt-2", 100),
|
||||
]
|
||||
)
|
||||
assert provider.token() == "jwt-1"
|
||||
clock["t"] = 85.0 # past the 80s refresh point
|
||||
assert provider.token() == "jwt-2"
|
||||
assert len(provider._opener.requests) == 2
|
||||
assert len(provider._opener.requests) == 2 # type: ignore[reportAttributeAccessIssue]
|
||||
|
||||
|
||||
def test_worker_key_provider_force_refresh():
|
||||
provider = WorkerKeyTokenProvider("http://users", "k", timeout=5, now=lambda: 0.0)
|
||||
provider._opener = SeqOpener([
|
||||
_exchange_response("jwt-1", 100),
|
||||
_exchange_response("jwt-2", 100),
|
||||
])
|
||||
provider._opener = SeqOpener( # type: ignore[reportAttributeAccessIssue]
|
||||
[
|
||||
_exchange_response("jwt-1", 100),
|
||||
_exchange_response("jwt-2", 100),
|
||||
]
|
||||
)
|
||||
assert provider.token() == "jwt-1"
|
||||
provider.refresh()
|
||||
assert provider.token() == "jwt-2"
|
||||
@@ -105,14 +111,18 @@ def test_worker_key_provider_force_refresh():
|
||||
|
||||
def test_worker_key_provider_raises_on_rejected_key():
|
||||
provider = WorkerKeyTokenProvider("http://users", "bad", timeout=5, now=lambda: 0.0)
|
||||
provider._opener = SeqOpener([HTTPError("http://users", 401, "unauthorized", {}, None)])
|
||||
provider._opener = SeqOpener( # type: ignore[reportAttributeAccessIssue]
|
||||
[HTTPError("http://users", 401, "unauthorized", {}, None)] # type: ignore[reportArgumentType]
|
||||
)
|
||||
with pytest.raises(TokenExchangeError):
|
||||
provider.token()
|
||||
|
||||
|
||||
def test_worker_key_provider_raises_when_token_missing():
|
||||
provider = WorkerKeyTokenProvider("http://users", "k", timeout=5, now=lambda: 0.0)
|
||||
provider._opener = SeqOpener([FakeResponse(200, json.dumps({"expires_in": 100}).encode())])
|
||||
provider._opener = SeqOpener( # type: ignore[reportAttributeAccessIssue]
|
||||
[FakeResponse(200, json.dumps({"expires_in": 100}).encode())]
|
||||
)
|
||||
with pytest.raises(TokenExchangeError):
|
||||
provider.token()
|
||||
|
||||
@@ -152,10 +162,12 @@ class RefreshCountingProvider:
|
||||
def test_coordinator_client_refreshes_and_retries_once_on_401():
|
||||
provider = RefreshCountingProvider()
|
||||
client = HttpCoordinatorClient("http://coord", timeout=5, token_provider=provider)
|
||||
client._opener = SeqOpener([
|
||||
HTTPError("http://coord/tasks/claim", 401, "unauthorized", {}, None),
|
||||
FakeResponse(204, b""),
|
||||
])
|
||||
client._opener = SeqOpener( # type: ignore[reportAttributeAccessIssue]
|
||||
[
|
||||
HTTPError("http://coord/tasks/claim", 401, "unauthorized", {}, None), # type: ignore[reportArgumentType]
|
||||
FakeResponse(204, b""),
|
||||
]
|
||||
)
|
||||
|
||||
status, _ = client._request("POST", "/tasks/claim", {"worker_id": "w"})
|
||||
|
||||
@@ -168,10 +180,12 @@ def test_coordinator_client_refreshes_and_retries_once_on_401():
|
||||
def test_coordinator_client_does_not_loop_on_persistent_401():
|
||||
provider = RefreshCountingProvider()
|
||||
client = HttpCoordinatorClient("http://coord", timeout=5, token_provider=provider)
|
||||
client._opener = SeqOpener([
|
||||
HTTPError("http://coord/x", 401, "unauthorized", {}, None),
|
||||
HTTPError("http://coord/x", 401, "unauthorized", {}, None),
|
||||
])
|
||||
client._opener = SeqOpener( # type: ignore[reportAttributeAccessIssue]
|
||||
[
|
||||
HTTPError("http://coord/x", 401, "unauthorized", {}, None), # type: ignore[reportArgumentType]
|
||||
HTTPError("http://coord/x", 401, "unauthorized", {}, None), # type: ignore[reportArgumentType]
|
||||
]
|
||||
)
|
||||
|
||||
status, _ = client._request("POST", "/x", {})
|
||||
|
||||
@@ -195,7 +209,9 @@ def test_worker_key_requires_userservice_url():
|
||||
|
||||
|
||||
def test_worker_key_with_userservice_url_is_valid():
|
||||
cfg = WorkerConfig(**_base_config(worker_key="scimesh_wk_live_x", userservice_url="http://users"))
|
||||
cfg = WorkerConfig(
|
||||
**_base_config(worker_key="scimesh_wk_live_x", userservice_url="http://users")
|
||||
)
|
||||
assert cfg.worker_key == "scimesh_wk_live_x"
|
||||
assert cfg.userservice_url == "http://users"
|
||||
|
||||
|
||||
@@ -479,7 +479,7 @@ def test_redirect_to_external_storage_strips_authorization() -> None:
|
||||
headers={"Authorization": "Bearer secret"},
|
||||
)
|
||||
redirected = handler.redirect_request(
|
||||
source, None, 302, "Found", {}, "https://bucket.example/presigned"
|
||||
source, None, 302, "Found", {}, "https://bucket.example/presigned" # type: ignore[arg-type]
|
||||
)
|
||||
assert redirected is not None
|
||||
assert redirected.get_header("Authorization") is None
|
||||
|
||||
Reference in New Issue
Block a user