Skip to content

Commit 07c7fbc

Browse files
committed
create domain model for jobs
1 parent 6cecc0f commit 07c7fbc

File tree

7 files changed

+136
-23
lines changed

7 files changed

+136
-23
lines changed

packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/projects.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,13 @@ async def get_project_marked_as_job(
8181
product_name: ProductName,
8282
user_id: UserID,
8383
project_uuid: ProjectID,
84-
job_parent_resource_name: str,
8584
) -> ProjectJobRpcGet:
8685
result = await rpc_client.request(
8786
WEBSERVER_RPC_NAMESPACE,
8887
TypeAdapter(RPCMethodName).validate_python("get_project_marked_as_job"),
8988
product_name=product_name,
9089
user_id=user_id,
9190
project_uuid=project_uuid,
92-
job_parent_resource_name=job_parent_resource_name,
9391
)
9492
assert TypeAdapter(ProjectJobRpcGet).validate_python(result) # nosec
9593
return cast(ProjectJobRpcGet, result)

services/api-server/src/simcore_service_api_server/_service_jobs.py

Lines changed: 23 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,11 @@
2222
from pydantic import HttpUrl
2323
from servicelib.logging_utils import log_context
2424

25+
from .models.api_resources import RelativeResourceName
2526
from .models.basic_types import NameValueTuple
26-
from .models.schemas.jobs import Job, JobID, JobInputs
27+
from .models.domain.jobs import Job as DomainJob
28+
from .models.schemas.jobs import Job as SchemaJob
29+
from .models.schemas.jobs import JobID, JobInputs
2730
from .models.schemas.programs import Program
2831
from .models.schemas.solvers import Solver
2932
from .services_http.solver_job_models_converters import (
@@ -57,7 +60,7 @@ async def list_jobs(
5760
filter_any_custom_metadata: list[NameValueTuple] | None = None,
5861
pagination_offset: PageOffsetInt | None = None,
5962
pagination_limit: PageLimitInt | None = None,
60-
) -> tuple[list[Job], PageMetaInfoLimitOffset]:
63+
) -> tuple[list[SchemaJob], PageMetaInfoLimitOffset]:
6164
"""Lists all jobs for a user with pagination based on resource name prefix"""
6265

6366
pagination_kwargs = as_dict_exclude_none(
@@ -74,7 +77,7 @@ async def list_jobs(
7477
)
7578

7679
# 2. Convert projects to jobs
77-
jobs: list[Job] = []
80+
jobs: list[SchemaJob] = []
7881
for project_job in projects_page.data:
7982
assert ( # nosec
8083
len(project_job.workbench) == 1
@@ -87,9 +90,9 @@ async def list_jobs(
8790
assert project_job.job_parent_resource_name # nosec
8891

8992
jobs.append(
90-
Job(
93+
SchemaJob(
9194
id=project_job.uuid,
92-
name=Job.compose_resource_name(
95+
name=SchemaJob.compose_resource_name(
9396
project_job.job_parent_resource_name, project_job.uuid
9497
),
9598
inputs_checksum=job_inputs.compute_checksum(),
@@ -114,12 +117,12 @@ async def create_job(
114117
hidden: bool,
115118
project_name: str | None,
116119
description: str | None,
117-
) -> tuple[Job, ProjectGet]:
120+
) -> tuple[SchemaJob, ProjectGet]:
118121
"""If no project_name is provided, the job name is used as project name"""
119122

120123
# creates NEW job as prototype
121124

122-
pre_job = Job.create_job_from_solver_or_program(
125+
pre_job = SchemaJob.create_job_from_solver_or_program(
123126
solver_or_program_name=solver_or_program.name, inputs=inputs
124127
)
125128
with log_context(
@@ -155,7 +158,7 @@ async def create_job(
155158
)
156159
assert job.id == pre_job.id # nosec
157160
assert job.name == pre_job.name # nosec
158-
assert job.name == Job.compose_resource_name(
161+
assert job.name == SchemaJob.compose_resource_name(
159162
parent_name=solver_or_program.resource_name,
160163
job_id=job.id,
161164
)
@@ -175,8 +178,18 @@ async def start_log_export(
175178
)
176179
return async_job_get
177180

181+
async def get_job(
182+
self,
183+
job_id: JobID,
184+
) -> DomainJob:
185+
return await self._web_rpc_client.get_project_marked_as_job(
186+
product_name=self.product_name,
187+
user_id=self.user_id,
188+
project_id=job_id,
189+
)
190+
178191
async def delete_job_assets(
179-
self, solver_or_program: Solver | Program, project_id: ProjectID
192+
self, job_parent_resource_name: RelativeResourceName, project_id: ProjectID
180193
):
181194
"""Marks job project as hidden and deletes S3 assets associated it"""
182195
await self._web_rest_client.patch_project(
@@ -189,6 +202,6 @@ async def delete_job_assets(
189202
product_name=self.product_name,
190203
user_id=self.user_id,
191204
project_uuid=project_id,
192-
job_parent_resource_name=solver_or_program.name,
205+
job_parent_resource_name=job_parent_resource_name,
193206
storage_data_deleted=True,
194207
)

services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from ..._service_jobs import JobService
1616
from ..._service_solvers import SolverService
17-
from ...exceptions.backend_errors import ProjectAlreadyStartedError
17+
from ...exceptions.backend_errors import JobNotFoundError, ProjectAlreadyStartedError
1818
from ...exceptions.service_errors_utils import DEFAULT_BACKEND_SERVICE_STATUS_CODES
1919
from ...models.basic_types import VersionStr
2020
from ...models.schemas.errors import ErrorGet
@@ -167,14 +167,16 @@ async def delete_job_assets(
167167
version: VersionStr,
168168
job_id: JobID,
169169
job_service: Annotated[JobService, Depends(get_job_service)],
170-
solver_service: Annotated[SolverService, Depends(get_solver_service)],
171170
):
172-
solver = await solver_service.get_solver(
173-
solver_key=solver_key,
174-
solver_version=version,
175-
)
171+
job = await job_service.get_job(job_id=job_id)
172+
if job.job_parent_resource_name != compose_job_resource_name(
173+
solver_key, version, job_id
174+
):
175+
raise JobNotFoundError(
176+
project_id=job_id,
177+
)
176178
await job_service.delete_job_assets(
177-
job_parent_resource_name=solver.name, project_id=job_id
179+
job_parent_resource_name=job.job_parent_resource_name, project_id=job_id
178180
)
179181

180182

services/api-server/src/simcore_service_api_server/exceptions/backend_errors.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ class ServiceForbiddenAccessError(BaseBackEndError):
5757
status_code = status.HTTP_403_FORBIDDEN
5858

5959

60+
class JobForbiddenAccessError(BaseBackEndError):
61+
msg_template = "Could not get solver/study job {project_id}"
62+
status_code = status.HTTP_403_FORBIDDEN
63+
64+
6065
class JobNotFoundError(BaseBackEndError):
6166
msg_template = "Could not get solver/study job {project_id}"
6267
status_code = status.HTTP_404_NOT_FOUND
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import datetime
2+
from typing import TypeAlias
3+
from uuid import UUID
4+
5+
from pydantic import (
6+
BaseModel,
7+
ConfigDict,
8+
Field,
9+
)
10+
11+
from ..api_resources import (
12+
RelativeResourceName,
13+
)
14+
15+
JobID: TypeAlias = UUID
16+
# JOB SUB-RESOURCES ----------
17+
#
18+
# - Wrappers for input/output values
19+
# - Input/outputs are defined in service metadata
20+
# - custom metadata
21+
#
22+
23+
24+
class Job(BaseModel):
25+
id: JobID
26+
name: RelativeResourceName
27+
job_parent_resource_name: RelativeResourceName
28+
29+
created_at: datetime.datetime = Field(..., description="Job creation timestamp")
30+
31+
# parent
32+
runner_name: RelativeResourceName = Field(
33+
..., description="Runner that executes job"
34+
)
35+
36+
model_config = ConfigDict(
37+
json_schema_extra={
38+
"example": {
39+
"id": "f622946d-fd29-35b9-a193-abdd1095167c",
40+
"name": "solvers/isolve/releases/1.3.4/jobs/f622946d-fd29-35b9-a193-abdd1095167c",
41+
"runner_name": "solvers/isolve/releases/1.3.4",
42+
"inputs_checksum": "12345",
43+
"created_at": "2021-01-22T23:59:52.322176",
44+
}
45+
}
46+
)

services/api-server/src/simcore_service_api_server/models/schemas/jobs.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
from ..basic_types import VersionStr
3939
from ..domain.files import File as DomainFile
4040
from ..domain.files import FileInProgramJobData
41+
from ..domain.jobs import Job as DomainJob
42+
from ..domain.jobs import JobID
4143
from ..schemas.files import UserFile
4244
from .base import ApiServerInputSchema
4345

@@ -50,8 +52,6 @@
5052
from .programs import Program, ProgramKeyId
5153
from .solvers import Solver
5254

53-
JobID: TypeAlias = UUID
54-
5555
# ArgumentTypes are types used in the job inputs (see ResultsTypes)
5656
ArgumentTypes: TypeAlias = (
5757
File | StrictFloat | StrictInt | StrictBool | str | list | None
@@ -324,6 +324,11 @@ def resource_name(self) -> str:
324324
return self.name
325325

326326

327+
assert set(DomainJob.model_fields.keys()).issubset(
328+
set(Job.model_fields.keys())
329+
) # nosec
330+
331+
327332
def get_url(
328333
solver_or_program: Solver | Program, url_for: Callable[..., HttpUrl], job_id: JobID
329334
) -> HttpUrl | None:

services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
from models_library.rpc.webserver.projects import (
4545
ListProjectsMarkedAsJobRpcFilters,
4646
MetadataFilterItem,
47+
PageRpcProjectJobRpcGet,
4748
)
4849
from models_library.services_types import ServiceRunID
4950
from models_library.users import UserID
@@ -63,6 +64,10 @@
6364
NotEnoughAvailableSeatsError,
6465
)
6566
from servicelib.rabbitmq.rpc_interfaces.webserver import projects as projects_rpc
67+
from servicelib.rabbitmq.rpc_interfaces.webserver.errors import (
68+
ProjectForbiddenRpcError,
69+
ProjectNotFoundRpcError,
70+
)
6671
from servicelib.rabbitmq.rpc_interfaces.webserver.functions import (
6772
functions_rpc_interface,
6873
)
@@ -79,14 +84,21 @@
7984
release_licensed_item_for_wallet as _release_licensed_item_for_wallet,
8085
)
8186
from simcore_service_api_server.models.basic_types import NameValueTuple
87+
from simcore_service_api_server.models.domain.jobs import Job
8288

8389
from ..exceptions.backend_errors import (
8490
CanNotCheckoutServiceIsNotRunningError,
8591
InsufficientNumberOfSeatsError,
92+
JobForbiddenAccessError,
93+
JobNotFoundError,
8694
LicensedItemCheckoutNotFoundError,
8795
)
8896
from ..exceptions.service_errors_utils import service_exception_mapper
89-
from ..models.api_resources import RelativeResourceName
97+
from ..models.api_resources import (
98+
RelativeResourceName,
99+
compose_resource_name,
100+
split_resource_name,
101+
)
90102
from ..models.pagination import Page, PaginationParams
91103
from ..models.schemas.model_adapter import (
92104
LicensedItemCheckoutGet,
@@ -262,7 +274,7 @@ async def list_projects_marked_as_jobs(
262274
pagination_limit: int = 50,
263275
filter_by_job_parent_resource_name_prefix: str | None,
264276
filter_any_custom_metadata: list[NameValueTuple] | None,
265-
):
277+
) -> PageRpcProjectJobRpcGet:
266278
pagination_kwargs = as_dict_exclude_none(
267279
offset=pagination_offset, limit=pagination_limit
268280
)
@@ -287,6 +299,38 @@ async def list_projects_marked_as_jobs(
287299
**pagination_kwargs,
288300
)
289301

302+
@_exception_mapper(
303+
rpc_exception_map={
304+
ProjectForbiddenRpcError: JobForbiddenAccessError,
305+
ProjectNotFoundRpcError: JobNotFoundError,
306+
}
307+
)
308+
async def get_project_marked_as_job(
309+
self,
310+
*,
311+
product_name: ProductName,
312+
user_id: UserID,
313+
project_id: ProjectID,
314+
) -> Job:
315+
project_job_rpc = await projects_rpc.get_project_marked_as_job(
316+
rpc_client=self._client,
317+
product_name=product_name,
318+
user_id=user_id,
319+
project_uuid=project_id,
320+
)
321+
collection_or_resource_ids = [
322+
*split_resource_name(project_job_rpc.job_parent_resource_name),
323+
"jobs",
324+
f"{project_job_rpc.uuid}",
325+
]
326+
return Job(
327+
id=project_job_rpc.uuid,
328+
name=compose_resource_name(*collection_or_resource_ids),
329+
job_parent_resource_name=project_job_rpc.job_parent_resource_name,
330+
created_at=project_job_rpc.created_at,
331+
runner_name=project_job_rpc.job_parent_resource_name,
332+
)
333+
290334
async def register_function(
291335
self, *, user_id: UserID, product_name: ProductName, function: Function
292336
) -> RegisteredFunction:

0 commit comments

Comments
 (0)