Skip to content

Commit a8863a9

Browse files
authored
refactor(jobs): use new input/output schemas (#126)
Inputs are now objects instead of just lists
1 parent 9ed4772 commit a8863a9

File tree

7 files changed

+71
-53
lines changed

7 files changed

+71
-53
lines changed

cryosparc/api.pyi

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ from .models.exposure import Exposure
2323
from .models.external import ExternalOutputSpec
2424
from .models.job import Job, JobStatus
2525
from .models.job_register import JobRegister
26-
from .models.job_spec import Category, InputSpec, InputSpecs, OutputResult, OutputSpec, OutputSpecs
26+
from .models.job_spec import Category, InputSpec, OutputResult, OutputSpec
2727
from .models.license import LicenseInstance, UpdateTag
2828
from .models.notification import Notification
2929
from .models.project import GenerateIntermediateResultsSettings, Project, ProjectSymlink
@@ -602,27 +602,13 @@ class JobsNamespace(APINamespace):
602602
"""
603603
...
604604
def get_log_path(self, project_uid: str, job_uid: str, /) -> str: ...
605-
def get_input_specs(self, project_uid: str, job_uid: str, /) -> InputSpecs: ...
606-
def get_input_spec(self, project_uid: str, job_uid: str, input_name: str, /) -> InputSpec: ...
607-
def add_external_input(self, project_uid: str, job_uid: str, input_name: str, /, body: InputSpec) -> Job:
608-
"""
609-
Add or replace an external job's input.
610-
"""
611-
...
612-
def get_output_specs(self, project_uid: str, job_uid: str, /) -> OutputSpecs: ...
613605
def get_output_fields(
614606
self, project_uid: str, job_uid: str, output_name: str, /, dtype_params: dict = {}
615607
) -> List[Tuple[str, str]]:
616608
"""
617609
Expected dataset column definitions for given job output, excluding passthroughs.
618610
"""
619611
...
620-
def get_output_spec(self, project_uid: str, job_uid: str, output_name: str, /) -> OutputSpec: ...
621-
def add_external_output(self, project_uid: str, job_uid: str, output_name: str, /, body: OutputSpec) -> Job:
622-
"""
623-
Add or replace an external job's output.
624-
"""
625-
...
626612
def create(
627613
self,
628614
project_uid: str,
@@ -758,6 +744,16 @@ class JobsNamespace(APINamespace):
758744
Removes an output result connected within the given input connection.
759745
"""
760746
...
747+
def add_external_input(self, project_uid: str, job_uid: str, input_name: str, /, body: InputSpec) -> Job:
748+
"""
749+
Add or replace an external job's input.
750+
"""
751+
...
752+
def add_external_output(self, project_uid: str, job_uid: str, output_name: str, /, body: OutputSpec) -> Job:
753+
"""
754+
Add or replace an external job's output.
755+
"""
756+
...
761757
def enqueue(
762758
self,
763759
project_uid: str,

cryosparc/controllers/job.py

Lines changed: 9 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1020,19 +1020,18 @@ def print_input_spec(self):
10201020
| | | | alignments2D | alignments2D | ✕
10211021
| | | | alignments3D | alignments3D | ✕
10221022
"""
1023-
specs = self.cs.api.jobs.get_input_specs(self.project_uid, self.uid)
10241023
headings = ["Input", "Title", "Type", "Required?", "Input Slots", "Slot Types", "Slot Required?"]
10251024
rows = []
1026-
for key, spec in specs.root.items():
1027-
name, title, type = key, spec.title, spec.type
1028-
required = f"✓ ({spec.count_min}" if spec.count_min else "✕ (0"
1029-
if spec.count_max in (0, "inf"):
1025+
for key, input in self.model.spec.inputs.root.items():
1026+
name, title, type = key, input.title, input.type
1027+
required = f"✓ ({input.count_min}" if input.count_min else "✕ (0"
1028+
if input.count_max in (0, "inf"):
10301029
required += "+)" # unlimited connections
1031-
elif spec.count_min == spec.count_max:
1030+
elif input.count_min == input.count_max:
10321031
required += ")"
10331032
else:
1034-
required += f"-{spec.count_max})"
1035-
for slot in spec.slots:
1033+
required += f"-{input.count_max})"
1034+
for slot in input.slots:
10361035
slot = as_input_slot(slot)
10371036
rows.append([name, title, type, required, slot.name, slot.dtype, "✓" if slot.required else "✕"])
10381037
name, title, type, required = ("",) * 4 # only show group info on first iter
@@ -1061,15 +1060,10 @@ def print_output_spec(self):
10611060
particles | Particles | particle | blob | blob | ✕
10621061
| | | ctf | ctf | ✕
10631062
"""
1064-
specs = self.cs.api.jobs.get_output_specs(self.project_uid, self.uid)
10651063
headings = ["Output", "Title", "Type", "Result Slots", "Result Types", "Passthrough?"]
10661064
rows = []
1067-
for key, spec in specs.root.items():
1068-
output = self.model.spec.outputs.root.get(key)
1069-
if not output:
1070-
warnings.warn(f"No results for input {key}", stacklevel=2)
1071-
continue
1072-
name, title, type = key, spec.title, spec.type
1065+
for key, output in self.model.spec.outputs.root.items():
1066+
name, title, type = key, output.title, output.type
10731067
for result in output.results:
10741068
rows.append([name, title, type, result.name, result.dtype, "✓" if result.passthrough else "✕"])
10751069
name, title, type = "", "", "" # only these print once per group

cryosparc/models/job_spec.py

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -59,22 +59,6 @@ class Connection(BaseModel):
5959

6060
job_uid: str
6161
output: str
62-
type: Literal[
63-
"exposure",
64-
"particle",
65-
"template",
66-
"volume",
67-
"volume_multi",
68-
"mask",
69-
"live",
70-
"ml_model",
71-
"symmetry_candidate",
72-
"flex_mesh",
73-
"flex_model",
74-
"hyperparameter",
75-
"denoise_model",
76-
"annotation_model",
77-
]
7862
results: List[InputResult] = []
7963

8064

@@ -135,6 +119,32 @@ class InputSlot(BaseModel):
135119
required: bool = False
136120

137121

122+
class Input(BaseModel):
123+
type: Literal[
124+
"exposure",
125+
"particle",
126+
"template",
127+
"volume",
128+
"volume_multi",
129+
"mask",
130+
"live",
131+
"ml_model",
132+
"symmetry_candidate",
133+
"flex_mesh",
134+
"flex_model",
135+
"hyperparameter",
136+
"denoise_model",
137+
"annotation_model",
138+
]
139+
title: str
140+
description: str = ""
141+
slots: List[InputSlot] = []
142+
count_min: int = 0
143+
count_max: Union[int, str] = "inf"
144+
repeat_allowed: bool = False
145+
connections: List[Connection] = []
146+
147+
138148
class InputSpec(BaseModel):
139149
type: Literal[
140150
"exposure",
@@ -165,7 +175,7 @@ class InputSpecs(RootModel):
165175

166176

167177
class Inputs(RootModel):
168-
root: Dict[str, List[Connection]] = {}
178+
root: Dict[str, Input] = {}
169179

170180

171181
class Params(BaseModel):
@@ -211,6 +221,11 @@ class Output(BaseModel):
211221
"denoise_model",
212222
"annotation_model",
213223
]
224+
title: str
225+
description: str = ""
226+
slots: List[OutputSlot] = []
227+
passthrough: Optional[str] = None
228+
passthrough_exclude_slots: List[str] = []
214229
results: List[OutputResult] = []
215230
num_items: int = 0
216231
image: Optional[str] = None

cryosparc/models/user.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class Bookmark(BaseModel):
1313
description: str
1414
color: str
1515
last_accessed: datetime.datetime
16-
created_at: datetime.datetime = datetime.datetime(2025, 3, 6, 16, 44, 32, 992643, tzinfo=datetime.timezone.utc)
16+
created_at: datetime.datetime
1717

1818

1919
class Email(BaseModel):

tests/conftest.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from cryosparc.models.job import Job
1717
from cryosparc.models.job_spec import (
1818
Connection,
19+
Input,
1920
InputResult,
2021
Inputs,
2122
JobSpec,
@@ -292,18 +293,26 @@ def mock_new_job(mock_user, mock_project):
292293
spec=JobSpec(
293294
type="homo_abinit",
294295
params=Params(),
295-
inputs=Inputs({"particles": []}),
296+
inputs=Inputs({"particles": Input(type="particle", title="Particles")}),
296297
outputs=Outputs(
297298
{
298299
"particles_class_0": Output(
299300
type="particle",
301+
title="Particles Class 1",
302+
passthrough="particles",
300303
results=[
301304
OutputResult(name="blob", dtype="blob"),
302305
OutputResult(name="ctf", dtype="ctf"),
303306
OutputResult(name="alignments3D", dtype="alignments3D"),
304307
],
305308
),
306-
"volume_class_0": Output(type="volume", results=[OutputResult(name="map", dtype="blob")]),
309+
"volume_class_0": Output(
310+
type="volume",
311+
title="Volume Class 0",
312+
results=[
313+
OutputResult(name="map", dtype="blob"),
314+
],
315+
),
307316
}
308317
),
309318
ui_tile_width=1,
@@ -330,7 +339,6 @@ def mock_new_job_with_params(mock_new_job: Job, mock_params: Params):
330339
def mock_new_job_with_connection(mock_new_job_with_params: Job):
331340
job = mock_new_job_with_params.model_copy(deep=True)
332341
input_particles = Connection(
333-
type="particle",
334342
job_uid="J41",
335343
output="particles",
336344
results=[
@@ -341,7 +349,7 @@ def mock_new_job_with_connection(mock_new_job_with_params: Job):
341349
],
342350
)
343351
passthrough_result = OutputResult(name="location", dtype="location", passthrough=True)
344-
job.spec.inputs.root["particles"] = [input_particles]
352+
job.spec.inputs.root["particles"] = Input(type="particle", title="Particles", connections=[input_particles])
345353
job.spec.outputs.root["particles_class_0"].results.append(passthrough_result)
346354
return job
347355

@@ -352,6 +360,8 @@ def mock_job(mock_new_job_with_connection: Job): # completed
352360
# fmt: off
353361
output_particles_class_0 = Output(
354362
type="particle",
363+
title="Particles Class 0",
364+
passthrough="particles",
355365
results=[
356366
OutputResult(
357367
name="blob",
@@ -387,6 +397,7 @@ def mock_job(mock_new_job_with_connection: Job): # completed
387397
)
388398
output_volume_class_0 = Output(
389399
type="volume",
400+
title="Volume Class 0",
390401
results=[
391402
OutputResult(
392403
name="map",

tests/controllers/test_job.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,7 @@ def external_job_with_added_output(external_job: ExternalJobController, mock_ext
134134
mock_external_job = mock_external_job.model_copy(deep=True)
135135
mock_external_job.spec.outputs.root["particles"] = Output(
136136
type="particle",
137+
title="Particles",
137138
results=[
138139
OutputResult(name="blob", dtype="blob"),
139140
OutputResult(name="ctf", dtype="ctf"),
@@ -150,6 +151,7 @@ def mock_external_job_with_saved_output(external_job_with_added_output: External
150151
mock_external_job = mock_external_job.model_copy(deep=True)
151152
mock_external_job.spec.outputs.root["particles"] = Output(
152153
type="particle",
154+
title="Particles",
153155
results=[
154156
OutputResult(name="blob", dtype="blob", versions=[0], metafiles=[metafile], num_items=[10]),
155157
OutputResult(name="ctf", dtype="ctf", versions=[0], metafiles=[metafile], num_items=[10]),

tests/test_tools.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def test_create_job_connect_params(
4141
assert isinstance(job, JobController)
4242
assert job.uid == mock_new_job_with_connection.uid
4343
assert job.model.spec.params == mock_params
44-
assert len(job.model.spec.inputs.root["particles"]) == 1
44+
assert len(job.model.spec.inputs.root["particles"].connections) == 1
4545
mock_create_endpoint.assert_called_once_with(
4646
project.uid, "W1", type="homo_abinit", title="", description="", params=mock_params.model_dump()
4747
)

0 commit comments

Comments
 (0)