Skip to content

Commit 8f141ce

Browse files
👽️ Add endpoint for getting credit-price and study-job log files from api server (#5985)
1 parent 4bbb76c commit 8f141ce

26 files changed

+971
-677
lines changed

packages/pytest-simcore/src/pytest_simcore/helpers/httpx_calls_capture_models.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class HttpApiCallCaptureModel(BaseModel):
2424
path: PathDescription | str
2525
query: str | None = None
2626
request_payload: dict[str, Any] | None = None
27-
response_body: dict[str, Any] | list | None = None
27+
response_body: list[Any] | dict[str, Any] | None = None
2828
status_code: HTTPStatus = Field(default=status.HTTP_200_OK)
2929

3030
@classmethod

services/api-server/openapi.json

Lines changed: 152 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"info": {
44
"title": "osparc.io web API",
55
"description": "osparc-simcore public API specifications",
6-
"version": "0.5.1"
6+
"version": "0.6.0"
77
},
88
"paths": {
99
"/v0/meta": {
@@ -2175,7 +2175,7 @@
21752175
"solvers"
21762176
],
21772177
"summary": "Start Job",
2178-
"description": "Starts job job_id created with the solver solver_key:version\n\nNew in *version 0.4.3*: cluster_id",
2178+
"description": "Starts job job_id created with the solver solver_key:version\n\nNew in *version 0.4.3*: cluster_id\nNew in *version 0.6.0*: This endpoint responds with a 202 when successfully starting a computation",
21792179
"operationId": "start_job",
21802180
"parameters": [
21812181
{
@@ -3571,6 +3571,7 @@
35713571
"studies"
35723572
],
35733573
"summary": "Start Study Job",
3574+
"description": "New in *version 0.6.0*: This endpoint responds with a 202 when successfully starting a computation",
35743575
"operationId": "start_study_job",
35753576
"parameters": [
35763577
{
@@ -3896,6 +3897,90 @@
38963897
}
38973898
]
38983899
}
3900+
},
3901+
"/v0/studies/{study_id}/jobs/{job_id}/outputs/log-links": {
3902+
"get": {
3903+
"tags": [
3904+
"studies"
3905+
],
3906+
"summary": "Get download links for study job log files",
3907+
"operationId": "get_study_job_output_logfile",
3908+
"parameters": [
3909+
{
3910+
"required": true,
3911+
"schema": {
3912+
"type": "string",
3913+
"format": "uuid",
3914+
"title": "Study Id"
3915+
},
3916+
"name": "study_id",
3917+
"in": "path"
3918+
},
3919+
{
3920+
"required": true,
3921+
"schema": {
3922+
"type": "string",
3923+
"format": "uuid",
3924+
"title": "Job Id"
3925+
},
3926+
"name": "job_id",
3927+
"in": "path"
3928+
}
3929+
],
3930+
"responses": {
3931+
"200": {
3932+
"description": "Successful Response",
3933+
"content": {
3934+
"application/json": {
3935+
"schema": {
3936+
"$ref": "#/components/schemas/JobLogsMap"
3937+
}
3938+
}
3939+
}
3940+
},
3941+
"422": {
3942+
"description": "Validation Error",
3943+
"content": {
3944+
"application/json": {
3945+
"schema": {
3946+
"$ref": "#/components/schemas/HTTPValidationError"
3947+
}
3948+
}
3949+
}
3950+
}
3951+
},
3952+
"security": [
3953+
{
3954+
"HTTPBasic": []
3955+
}
3956+
]
3957+
}
3958+
},
3959+
"/v0/credits/price": {
3960+
"get": {
3961+
"tags": [
3962+
"credits"
3963+
],
3964+
"summary": "Get Credits Price",
3965+
"operationId": "get_credits_price",
3966+
"responses": {
3967+
"200": {
3968+
"description": "Successful Response",
3969+
"content": {
3970+
"application/json": {
3971+
"schema": {
3972+
"$ref": "#/components/schemas/GetCreditPrice"
3973+
}
3974+
}
3975+
}
3976+
}
3977+
},
3978+
"security": [
3979+
{
3980+
"HTTPBasic": []
3981+
}
3982+
]
3983+
}
38993984
}
39003985
},
39013986
"components": {
@@ -4097,6 +4182,33 @@
40974182
],
40984183
"title": "FileUploadData"
40994184
},
4185+
"GetCreditPrice": {
4186+
"properties": {
4187+
"productName": {
4188+
"type": "string",
4189+
"title": "Productname"
4190+
},
4191+
"usdPerCredit": {
4192+
"type": "number",
4193+
"minimum": 0.0,
4194+
"title": "Usdpercredit",
4195+
"description": "Price of a credit in USD. If None, then this product's price is UNDEFINED"
4196+
},
4197+
"minPaymentAmountUsd": {
4198+
"type": "integer",
4199+
"minimum": 0,
4200+
"title": "Minpaymentamountusd",
4201+
"description": "Minimum amount (included) in USD that can be paid for this productCan be None if this product's price is UNDEFINED"
4202+
}
4203+
},
4204+
"type": "object",
4205+
"required": [
4206+
"productName",
4207+
"usdPerCredit",
4208+
"minPaymentAmountUsd"
4209+
],
4210+
"title": "GetCreditPrice"
4211+
},
41004212
"Groups": {
41014213
"properties": {
41024214
"me": {
@@ -4259,6 +4371,23 @@
42594371
}
42604372
}
42614373
},
4374+
"JobLogsMap": {
4375+
"properties": {
4376+
"log_links": {
4377+
"items": {
4378+
"$ref": "#/components/schemas/LogLink"
4379+
},
4380+
"type": "array",
4381+
"title": "Log Links",
4382+
"description": "Array of download links"
4383+
}
4384+
},
4385+
"type": "object",
4386+
"required": [
4387+
"log_links"
4388+
],
4389+
"title": "JobLogsMap"
4390+
},
42624391
"JobOutputs": {
42634392
"properties": {
42644393
"job_id": {
@@ -4397,6 +4526,27 @@
43974526
"type": "object",
43984527
"title": "Links"
43994528
},
4529+
"LogLink": {
4530+
"properties": {
4531+
"node_name": {
4532+
"type": "string",
4533+
"title": "Node Name"
4534+
},
4535+
"download_link": {
4536+
"type": "string",
4537+
"maxLength": 65536,
4538+
"minLength": 1,
4539+
"format": "uri",
4540+
"title": "Download Link"
4541+
}
4542+
},
4543+
"type": "object",
4544+
"required": [
4545+
"node_name",
4546+
"download_link"
4547+
],
4548+
"title": "LogLink"
4549+
},
44004550
"Meta": {
44014551
"properties": {
44024552
"name": {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from fastapi import APIRouter
44

55
from ..core.settings import ApplicationSettings
6+
from .routes import credits as _credits
67
from .routes import (
78
files,
89
health,
@@ -38,6 +39,7 @@ def create_router(settings: ApplicationSettings):
3839
router.include_router(studies.router, tags=["studies"], prefix="/studies")
3940
router.include_router(studies_jobs.router, tags=["studies"], prefix="/studies")
4041
router.include_router(wallets.router, tags=["wallets"], prefix="/wallets")
42+
router.include_router(_credits.router, tags=["credits"], prefix="/credits")
4143

4244
# NOTE: multiple-files upload is currently disabled
4345
# Web form to upload files at http://localhost:8000/v0/upload-form-view
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from typing import Annotated
2+
3+
from fastapi import APIRouter, Depends, status
4+
from models_library.api_schemas_webserver.product import GetCreditPrice
5+
6+
from ..dependencies.webserver import AuthSession, get_webserver_session
7+
8+
router = APIRouter()
9+
10+
11+
@router.get("/price", status_code=status.HTTP_200_OK, response_model=GetCreditPrice)
12+
async def get_credits_price(
13+
webserver_api: Annotated[AuthSession, Depends(get_webserver_session)],
14+
):
15+
product_price = await webserver_api.get_product_price()
16+
return product_price

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from ...models.schemas.jobs import ArgumentTypes, Job, JobID, JobMetadata, JobOutputs
3232
from ...models.schemas.solvers import SolverKeyId
3333
from ...services.catalog import CatalogApi
34-
from ...services.director_v2 import DirectorV2Api, DownloadLink, NodeName
34+
from ...services.director_v2 import DirectorV2Api
3535
from ...services.jobs import (
3636
get_custom_metadata,
3737
raise_if_job_not_associated_with_solver,
@@ -231,7 +231,7 @@ async def get_job_outputs(
231231
assert len(node_ids) == 1 # nosec
232232

233233
product_price = await webserver_api.get_product_price()
234-
if product_price is not None:
234+
if product_price.usd_per_credit is not None:
235235
wallet = await webserver_api.get_project_wallet(project_id=project.uuid)
236236
if wallet is None:
237237
raise MissingWalletError(job_id=project.uuid)
@@ -295,24 +295,26 @@ async def get_job_output_logfile(
295295

296296
project_id = job_id
297297

298-
logs_urls: dict[NodeName, DownloadLink] = await director2_api.get_computation_logs(
298+
log_link_map = await director2_api.get_computation_logs(
299299
user_id=user_id, project_id=project_id
300300
)
301+
logs_urls = log_link_map.log_links
301302

302303
_logger.debug(
303304
"Found %d logfiles for %s %s: %s",
304305
len(logs_urls),
305306
f"{project_id=}",
306307
f"{user_id=}",
307-
list(logs_urls.keys()),
308+
list(elm.download_link for elm in logs_urls),
308309
)
309310

310311
# if more than one node? should rezip all of them??
311312
assert ( # nosec
312313
len(logs_urls) <= 1
313314
), "Current version only supports one node per solver"
314315

315-
for presigned_download_link in logs_urls.values():
316+
for log_link in logs_urls:
317+
presigned_download_link = log_link.download_link
316318
_logger.info(
317319
"Redirecting '%s' to %s ...",
318320
f"{solver_key}/releases/{version}/jobs/{job_id}/outputs/logfile",

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

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from fastapi import APIRouter, Depends, Header, Query, Request, status
66
from fastapi.encoders import jsonable_encoder
7-
from fastapi.responses import JSONResponse, RedirectResponse
7+
from fastapi.responses import JSONResponse
88
from models_library.api_schemas_webserver.projects import ProjectPatch
99
from models_library.api_schemas_webserver.projects_nodes import NodeOutputs
1010
from models_library.clusters import ClusterID
@@ -33,7 +33,7 @@
3333
JobOutputs,
3434
JobStatus,
3535
)
36-
from ...models.schemas.studies import Study, StudyID
36+
from ...models.schemas.studies import JobLogsMap, Study, StudyID
3737
from ...services.director_v2 import DirectorV2Api
3838
from ...services.jobs import (
3939
get_custom_metadata,
@@ -302,15 +302,27 @@ async def get_study_job_outputs(
302302
return job_outputs
303303

304304

305-
@router.post(
306-
"/{study_id}/jobs/{job_id}/outputs/logfile",
307-
response_class=RedirectResponse,
308-
include_in_schema=API_SERVER_DEV_FEATURES_ENABLED,
309-
status_code=status.HTTP_501_NOT_IMPLEMENTED,
305+
@router.get(
306+
"/{study_id}/jobs/{job_id}/outputs/log-links",
307+
response_model=JobLogsMap,
308+
status_code=status.HTTP_200_OK,
309+
summary="Get download links for study job log files",
310310
)
311-
async def get_study_job_output_logfile(study_id: StudyID, job_id: JobID):
312-
msg = f"get study job output logfile study_id={study_id!r} job_id={job_id!r}. SEE https://github.com/ITISFoundation/osparc-simcore/issues/4177"
313-
raise NotImplementedError(msg)
311+
async def get_study_job_output_logfile(
312+
study_id: StudyID,
313+
job_id: JobID,
314+
user_id: Annotated[PositiveInt, Depends(get_current_user_id)],
315+
director2_api: Annotated[DirectorV2Api, Depends(get_api_client(DirectorV2Api))],
316+
):
317+
with log_context(
318+
logger=_logger,
319+
level=logging.DEBUG,
320+
msg=f"get study job output logfile study_id={study_id!r} job_id={job_id!r}.",
321+
):
322+
log_link_map = await director2_api.get_computation_logs(
323+
user_id=user_id, project_id=job_id
324+
)
325+
return log_link_map
314326

315327

316328
@router.get(
Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1-
import typing
1+
from typing import TypeAlias
22

3-
import pydantic
43
from models_library import projects, projects_nodes_io
54
from models_library.utils import pydantic_tools_extension
5+
from pydantic import AnyUrl, BaseModel, Field
66

77
from .. import api_resources
88
from . import solvers
99

10-
StudyID: typing.TypeAlias = projects.ProjectID
10+
StudyID: TypeAlias = projects.ProjectID
11+
NodeName: TypeAlias = str
12+
DownloadLink: TypeAlias = AnyUrl
1113

1214

13-
class Study(pydantic.BaseModel):
15+
class Study(BaseModel):
1416
uid: StudyID
1517
title: str = pydantic_tools_extension.FieldNotRequired()
1618
description: str = pydantic_tools_extension.FieldNotRequired()
@@ -21,9 +23,18 @@ def compose_resource_name(cls, study_key) -> api_resources.RelativeResourceName:
2123

2224

2325
class StudyPort(solvers.SolverPort):
24-
key: projects_nodes_io.NodeID = pydantic.Field(
26+
key: projects_nodes_io.NodeID = Field(
2527
...,
2628
description="port identifier name."
2729
"Correponds to the UUID of the parameter/probe node in the study",
2830
title="Key name",
2931
)
32+
33+
34+
class LogLink(BaseModel):
35+
node_name: NodeName
36+
download_link: DownloadLink
37+
38+
39+
class JobLogsMap(BaseModel):
40+
log_links: list[LogLink] = Field(..., description="Array of download links")

0 commit comments

Comments
 (0)