diff --git a/packages/models-library/src/models_library/api_schemas_webserver/functions_wb_schema.py b/packages/models-library/src/models_library/api_schemas_webserver/functions_wb_schema.py new file mode 100644 index 00000000000..95dafedef38 --- /dev/null +++ b/packages/models-library/src/models_library/api_schemas_webserver/functions_wb_schema.py @@ -0,0 +1,290 @@ +from collections.abc import Mapping +from enum import Enum +from typing import Annotated, Any, Literal, TypeAlias +from uuid import UUID + +from common_library.errors_classes import OsparcErrorMixin +from models_library import projects +from models_library.services_types import ServiceKey, ServiceVersion +from pydantic import BaseModel, Field + +from ..projects import ProjectID + +FunctionID: TypeAlias = UUID +FunctionJobID: TypeAlias = UUID +FileID: TypeAlias = UUID + +InputTypes: TypeAlias = FileID | float | int | bool | str | list + + +class FunctionSchemaClass(str, Enum): + json_schema = "application/schema+json" + + +class FunctionSchemaBase(BaseModel): + schema_content: Any = Field(default=None) + schema_class: FunctionSchemaClass + + +class JSONFunctionSchema(FunctionSchemaBase): + schema_content: Mapping[str, Any] = Field( + default={}, description="JSON Schema", title="JSON Schema" + ) # json-schema library defines a schema as Mapping[str, Any] + schema_class: FunctionSchemaClass = FunctionSchemaClass.json_schema + + +class JSONFunctionInputSchema(JSONFunctionSchema): + schema_class: Literal[FunctionSchemaClass.json_schema] = ( + FunctionSchemaClass.json_schema + ) + + +class JSONFunctionOutputSchema(JSONFunctionSchema): + schema_class: Literal[FunctionSchemaClass.json_schema] = ( + FunctionSchemaClass.json_schema + ) + + +FunctionInputSchema: TypeAlias = Annotated[ + JSONFunctionInputSchema, + Field(discriminator="schema_class"), +] + +FunctionOutputSchema: TypeAlias = Annotated[ + JSONFunctionOutputSchema, + Field(discriminator="schema_class"), +] + + +class FunctionClass(str, Enum): + project = "project" + solver = "solver" + python_code = "python_code" + + +FunctionClassSpecificData: TypeAlias = dict[str, Any] +FunctionJobClassSpecificData: TypeAlias = FunctionClassSpecificData + + +# TODO, use InputTypes here, but api is throwing weird errors and asking for dict for elements # noqa: FIX002 +# see here https://github.com/ITISFoundation/osparc-simcore/issues/7659 +FunctionInputs: TypeAlias = dict[str, Any] | None + +FunctionInputsList: TypeAlias = list[FunctionInputs] + +FunctionOutputs: TypeAlias = dict[str, Any] | None + +FunctionOutputsLogfile: TypeAlias = Any + + +class FunctionBase(BaseModel): + function_class: FunctionClass + title: str = "" + description: str = "" + input_schema: FunctionInputSchema + output_schema: FunctionOutputSchema + default_inputs: FunctionInputs + + +class RegisteredFunctionBase(FunctionBase): + uid: FunctionID + + +class ProjectFunction(FunctionBase): + function_class: Literal[FunctionClass.project] = FunctionClass.project + project_id: ProjectID + + +class RegisteredProjectFunction(ProjectFunction, RegisteredFunctionBase): + pass + + +SolverJobID: TypeAlias = UUID + + +class SolverFunction(FunctionBase): + function_class: Literal[FunctionClass.solver] = FunctionClass.solver + solver_key: ServiceKey + solver_version: ServiceVersion + + +class RegisteredSolverFunction(SolverFunction, RegisteredFunctionBase): + pass + + +class PythonCodeFunction(FunctionBase): + function_class: Literal[FunctionClass.python_code] = FunctionClass.python_code + code_url: str + + +class RegisteredPythonCodeFunction(PythonCodeFunction, RegisteredFunctionBase): + pass + + +Function: TypeAlias = Annotated[ + ProjectFunction | PythonCodeFunction | SolverFunction, + Field(discriminator="function_class"), +] +RegisteredFunction: TypeAlias = Annotated[ + RegisteredProjectFunction | RegisteredPythonCodeFunction | RegisteredSolverFunction, + Field(discriminator="function_class"), +] + +FunctionJobCollectionID: TypeAlias = projects.ProjectID + + +class FunctionJobBase(BaseModel): + title: str = "" + description: str = "" + function_uid: FunctionID + inputs: FunctionInputs + outputs: FunctionOutputs + function_class: FunctionClass + + +class RegisteredFunctionJobBase(FunctionJobBase): + uid: FunctionJobID + + +class ProjectFunctionJob(FunctionJobBase): + function_class: Literal[FunctionClass.project] = FunctionClass.project + project_job_id: ProjectID + + +class RegisteredProjectFunctionJob(ProjectFunctionJob, RegisteredFunctionJobBase): + pass + + +class SolverFunctionJob(FunctionJobBase): + function_class: Literal[FunctionClass.solver] = FunctionClass.solver + solver_job_id: ProjectID + + +class RegisteredSolverFunctionJob(SolverFunctionJob, RegisteredFunctionJobBase): + pass + + +class PythonCodeFunctionJob(FunctionJobBase): + function_class: Literal[FunctionClass.python_code] = FunctionClass.python_code + + +class RegisteredPythonCodeFunctionJob(PythonCodeFunctionJob, RegisteredFunctionJobBase): + pass + + +FunctionJob: TypeAlias = Annotated[ + ProjectFunctionJob | PythonCodeFunctionJob | SolverFunctionJob, + Field(discriminator="function_class"), +] + +RegisteredFunctionJob: TypeAlias = Annotated[ + RegisteredProjectFunctionJob + | RegisteredPythonCodeFunctionJob + | RegisteredSolverFunctionJob, + Field(discriminator="function_class"), +] + + +class FunctionJobStatus(BaseModel): + status: str + + +class FunctionJobCollection(BaseModel): + """Model for a collection of function jobs""" + + title: str = "" + description: str = "" + job_ids: list[FunctionJobID] = [] + + +class RegisteredFunctionJobCollection(FunctionJobCollection): + uid: FunctionJobCollectionID + + +class FunctionJobCollectionStatus(BaseModel): + status: list[str] + + +class FunctionBaseError(OsparcErrorMixin, Exception): + pass + + +class FunctionIDNotFoundError(FunctionBaseError): + """Exception raised when a function is not found""" + + msg_template: str = "Function {function_id} not found" + + +class FunctionJobIDNotFoundError(FunctionBaseError): + """Exception raised when a function job is not found""" + + msg_template: str = "Function job {function_job_id} not found" + + +class FunctionJobCollectionIDNotFoundError(FunctionBaseError): + """Exception raised when a function job collection is not found""" + + msg_template: str = "Function job collection {function_job_collection_id} not found" + + +class UnsupportedFunctionClassError(FunctionBaseError): + """Exception raised when a function class is not supported""" + + msg_template: str = "Function class {function_class} is not supported" + + +class UnsupportedFunctionJobClassError(FunctionBaseError): + """Exception raised when a function job class is not supported""" + + msg_template: str = "Function job class {function_job_class} is not supported" + + +class UnsupportedFunctionFunctionJobClassCombinationError(FunctionBaseError): + """Exception raised when a function / function job class combination is not supported""" + + msg_template: str = ( + "Function class {function_class} and function job class {function_job_class} combination is not supported" + ) + + +class FunctionInputsValidationError(FunctionBaseError): + """Exception raised when validating function inputs""" + + msg_template: str = "Function inputs validation failed: {error}" + + +class FunctionJobDB(BaseModel): + function_uuid: FunctionID + title: str = "" + description: str = "" + inputs: FunctionInputs + outputs: FunctionOutputs + class_specific_data: FunctionJobClassSpecificData + function_class: FunctionClass + + +class RegisteredFunctionJobDB(FunctionJobDB): + uuid: FunctionJobID + + +class FunctionDB(BaseModel): + function_class: FunctionClass + title: str = "" + description: str = "" + input_schema: FunctionInputSchema + output_schema: FunctionOutputSchema + default_inputs: FunctionInputs + class_specific_data: FunctionClassSpecificData + + +class RegisteredFunctionDB(FunctionDB): + uuid: FunctionID + + +class FunctionJobCollectionDB(BaseModel): + title: str = "" + description: str = "" + + +class RegisteredFunctionJobCollectionDB(FunctionJobCollectionDB): + uuid: FunctionJobCollectionID diff --git a/packages/postgres-database/src/simcore_postgres_database/migration/versions/44f40f1069aa_add_function_api_tables.py b/packages/postgres-database/src/simcore_postgres_database/migration/versions/44f40f1069aa_add_function_api_tables.py new file mode 100644 index 00000000000..a95d60ec226 --- /dev/null +++ b/packages/postgres-database/src/simcore_postgres_database/migration/versions/44f40f1069aa_add_function_api_tables.py @@ -0,0 +1,125 @@ +"""Add function api tables + +Revision ID: 44f40f1069aa +Revises: 0d52976dc616 +Create Date: 2025-05-09 13:12:31.423832+00:00 + +""" + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = "44f40f1069aa" +down_revision = "0d52976dc616" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "funcapi_function_job_collections", + sa.Column("uuid", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("title", sa.String(), nullable=True), + sa.Column("description", sa.String(), nullable=True), + sa.PrimaryKeyConstraint("uuid", name="funcapi_function_job_collections_pk"), + ) + op.create_index( + op.f("ix_funcapi_function_job_collections_uuid"), + "funcapi_function_job_collections", + ["uuid"], + unique=False, + ) + op.create_table( + "funcapi_functions", + sa.Column("uuid", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("title", sa.String(), nullable=True), + sa.Column("function_class", sa.String(), nullable=True), + sa.Column("description", sa.String(), nullable=True), + sa.Column("input_schema", sa.JSON(), nullable=True), + sa.Column("output_schema", sa.JSON(), nullable=True), + sa.Column("system_tags", sa.JSON(), nullable=True), + sa.Column("user_tags", sa.JSON(), nullable=True), + sa.Column("class_specific_data", sa.JSON(), nullable=True), + sa.Column("default_inputs", sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint("uuid", name="funcapi_functions_pk"), + ) + op.create_index( + op.f("ix_funcapi_functions_uuid"), "funcapi_functions", ["uuid"], unique=False + ) + op.create_table( + "funcapi_function_jobs", + sa.Column("uuid", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("title", sa.String(), nullable=True), + sa.Column("function_uuid", postgresql.UUID(as_uuid=True), nullable=False), + sa.Column("function_class", sa.String(), nullable=True), + sa.Column("status", sa.String(), nullable=True), + sa.Column("inputs", sa.JSON(), nullable=True), + sa.Column("outputs", sa.JSON(), nullable=True), + sa.Column("class_specific_data", sa.JSON(), nullable=True), + sa.ForeignKeyConstraint( + ["function_uuid"], + ["funcapi_functions.uuid"], + name="fk_function_jobs_to_function_uuid", + onupdate="CASCADE", + ondelete="CASCADE", + ), + sa.PrimaryKeyConstraint("uuid", name="funcapi_function_jobs_pk"), + ) + op.create_index( + op.f("ix_funcapi_function_jobs_function_uuid"), + "funcapi_function_jobs", + ["function_uuid"], + unique=False, + ) + op.create_index( + op.f("ix_funcapi_function_jobs_uuid"), + "funcapi_function_jobs", + ["uuid"], + unique=False, + ) + op.create_table( + "funcapi_function_job_collections_to_function_jobs", + sa.Column( + "function_job_collection_uuid", postgresql.UUID(as_uuid=True), nullable=True + ), + sa.Column("function_job_uuid", postgresql.UUID(as_uuid=True), nullable=True), + sa.ForeignKeyConstraint( + ["function_job_collection_uuid"], + ["funcapi_function_job_collections.uuid"], + name="fk_func_job_coll_to_func_jobs_to_func_job_coll_uuid", + onupdate="CASCADE", + ondelete="CASCADE", + ), + sa.ForeignKeyConstraint( + ["function_job_uuid"], + ["funcapi_function_jobs.uuid"], + name="fk_func_job_coll_to_func_jobs_to_func_job_uuid", + onupdate="CASCADE", + ondelete="CASCADE", + ), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("funcapi_function_job_collections_to_function_jobs") + op.drop_index( + op.f("ix_funcapi_function_jobs_uuid"), table_name="funcapi_function_jobs" + ) + op.drop_index( + op.f("ix_funcapi_function_jobs_function_uuid"), + table_name="funcapi_function_jobs", + ) + op.drop_table("funcapi_function_jobs") + op.drop_index(op.f("ix_funcapi_functions_uuid"), table_name="funcapi_functions") + op.drop_table("funcapi_functions") + op.drop_index( + op.f("ix_funcapi_function_job_collections_uuid"), + table_name="funcapi_function_job_collections", + ) + op.drop_table("funcapi_function_job_collections") + # ### end Alembic commands ### diff --git a/packages/postgres-database/src/simcore_postgres_database/migration/versions/7cefc13e3b2b_function_db_details.py b/packages/postgres-database/src/simcore_postgres_database/migration/versions/7cefc13e3b2b_function_db_details.py new file mode 100644 index 00000000000..7bcb428923c --- /dev/null +++ b/packages/postgres-database/src/simcore_postgres_database/migration/versions/7cefc13e3b2b_function_db_details.py @@ -0,0 +1,72 @@ +"""Function db details + +Revision ID: 7cefc13e3b2b +Revises: 44f40f1069aa +Create Date: 2025-05-12 11:28:32.298331+00:00 + +""" + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision = "7cefc13e3b2b" +down_revision = "44f40f1069aa" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column( + "funcapi_function_job_collections", + sa.Column( + "created", sa.DateTime(), server_default=sa.text("now()"), nullable=False + ), + ) + op.add_column( + "funcapi_function_job_collections", + sa.Column( + "modified", sa.DateTime(), server_default=sa.text("now()"), nullable=False + ), + ) + op.add_column( + "funcapi_function_job_collections_to_function_jobs", + sa.Column( + "created", sa.DateTime(), server_default=sa.text("now()"), nullable=False + ), + ) + op.add_column( + "funcapi_function_job_collections_to_function_jobs", + sa.Column( + "modified", sa.DateTime(), server_default=sa.text("now()"), nullable=False + ), + ) + op.add_column( + "funcapi_function_jobs", sa.Column("description", sa.String(), nullable=True) + ) + op.add_column( + "funcapi_function_jobs", + sa.Column( + "created", sa.DateTime(), server_default=sa.text("now()"), nullable=False + ), + ) + op.add_column( + "funcapi_function_jobs", + sa.Column( + "modified", sa.DateTime(), server_default=sa.text("now()"), nullable=False + ), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column("funcapi_function_jobs", "modified") + op.drop_column("funcapi_function_jobs", "created") + op.drop_column("funcapi_function_jobs", "description") + op.drop_column("funcapi_function_job_collections_to_function_jobs", "modified") + op.drop_column("funcapi_function_job_collections_to_function_jobs", "created") + op.drop_column("funcapi_function_job_collections", "modified") + op.drop_column("funcapi_function_job_collections", "created") + # ### end Alembic commands ### diff --git a/packages/postgres-database/src/simcore_postgres_database/models/funcapi_function_job_collections_table.py b/packages/postgres-database/src/simcore_postgres_database/models/funcapi_function_job_collections_table.py new file mode 100644 index 00000000000..dcc4a08b9c4 --- /dev/null +++ b/packages/postgres-database/src/simcore_postgres_database/models/funcapi_function_job_collections_table.py @@ -0,0 +1,51 @@ +"""Functions table + +- List of functions served by the simcore platform +""" + +import uuid + +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.sql import func + +from .base import metadata + +function_job_collections_table = sa.Table( + "funcapi_function_job_collections", + metadata, + sa.Column( + "uuid", + UUID(as_uuid=True), + default=uuid.uuid4, + primary_key=True, + index=True, + doc="Unique id of the function job collection", + ), + sa.Column( + "title", + sa.String, + doc="Title of the function job collection", + ), + sa.Column( + "description", + sa.String, + doc="Description of the function job collection", + ), + sa.Column( + "created", + sa.DateTime(), + nullable=False, + server_default=func.now(), + doc="Timestamp auto-generated upon creation", + ), + sa.Column( + "modified", + sa.DateTime(), + nullable=False, + server_default=func.now(), + onupdate=func.now(), + doc="Automaticaly updates on modification of the row", + ), + sa.PrimaryKeyConstraint("uuid", name="funcapi_function_job_collections_pk"), +) diff --git a/packages/postgres-database/src/simcore_postgres_database/models/funcapi_function_job_collections_to_function_jobs_table.py b/packages/postgres-database/src/simcore_postgres_database/models/funcapi_function_job_collections_to_function_jobs_table.py new file mode 100644 index 00000000000..bc455affe94 --- /dev/null +++ b/packages/postgres-database/src/simcore_postgres_database/models/funcapi_function_job_collections_to_function_jobs_table.py @@ -0,0 +1,52 @@ +"""Functions table + +- List of functions served by the simcore platform +""" + +import sqlalchemy as sa +from sqlalchemy.sql import func + +from ._common import RefActions +from .base import metadata +from .funcapi_function_job_collections_table import function_job_collections_table +from .funcapi_function_jobs_table import function_jobs_table + +function_job_collections_to_function_jobs_table = sa.Table( + "funcapi_function_job_collections_to_function_jobs", + metadata, + sa.Column( + "function_job_collection_uuid", + sa.ForeignKey( + function_job_collections_table.c.uuid, + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, + name="fk_func_job_coll_to_func_jobs_to_func_job_coll_uuid", + ), + doc="Unique identifier of the function job collection", + ), + sa.Column( + "function_job_uuid", + sa.ForeignKey( + function_jobs_table.c.uuid, + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, + name="fk_func_job_coll_to_func_jobs_to_func_job_uuid", + ), + doc="Unique identifier of the function job", + ), + sa.Column( + "created", + sa.DateTime(), + nullable=False, + server_default=func.now(), + doc="Timestamp auto-generated upon creation", + ), + sa.Column( + "modified", + sa.DateTime(), + nullable=False, + server_default=func.now(), + onupdate=func.now(), + doc="Automaticaly updates on modification of the row", + ), +) diff --git a/packages/postgres-database/src/simcore_postgres_database/models/funcapi_function_jobs_table.py b/packages/postgres-database/src/simcore_postgres_database/models/funcapi_function_jobs_table.py new file mode 100644 index 00000000000..2a13decd621 --- /dev/null +++ b/packages/postgres-database/src/simcore_postgres_database/models/funcapi_function_jobs_table.py @@ -0,0 +1,91 @@ +"""Functions table + +- List of functions served by the simcore platform +""" + +import uuid + +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import UUID +from sqlalchemy.sql import func + +from ._common import RefActions +from .base import metadata +from .funcapi_functions_table import functions_table + +function_jobs_table = sa.Table( + "funcapi_function_jobs", + metadata, + sa.Column( + "uuid", + UUID(as_uuid=True), + primary_key=True, + index=True, + default=uuid.uuid4, + doc="Unique id of the function job", + ), + sa.Column( + "title", + sa.String, + doc="Name of the function job", + ), + sa.Column( + "description", + sa.String, + doc="Description of the function job", + ), + sa.Column( + "function_uuid", + sa.ForeignKey( + functions_table.c.uuid, + onupdate=RefActions.CASCADE, + ondelete=RefActions.CASCADE, + name="fk_function_jobs_to_function_uuid", + ), + nullable=False, + index=True, + doc="Unique identifier of the function", + ), + sa.Column( + "function_class", + sa.String, + doc="Class of the function", + ), + sa.Column( + "status", + sa.String, + doc="Status of the function job", + ), + sa.Column( + "inputs", + sa.JSON, + doc="Inputs of the function job", + ), + sa.Column( + "outputs", + sa.JSON, + doc="Outputs of the function job", + ), + sa.Column( + "class_specific_data", + sa.JSON, + nullable=True, + doc="Fields specific for a function class", + ), + sa.Column( + "created", + sa.DateTime(), + nullable=False, + server_default=func.now(), + doc="Timestamp auto-generated upon creation", + ), + sa.Column( + "modified", + sa.DateTime(), + nullable=False, + server_default=func.now(), + onupdate=func.now(), + doc="Automaticaly updates on modification of the row", + ), + sa.PrimaryKeyConstraint("uuid", name="funcapi_function_jobs_pk"), +) diff --git a/packages/postgres-database/src/simcore_postgres_database/models/funcapi_functions_table.py b/packages/postgres-database/src/simcore_postgres_database/models/funcapi_functions_table.py new file mode 100644 index 00000000000..a7cb97740a9 --- /dev/null +++ b/packages/postgres-database/src/simcore_postgres_database/models/funcapi_functions_table.py @@ -0,0 +1,74 @@ +"""Functions table + +- List of functions served by the simcore platform +""" + +import uuid + +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import UUID + +from .base import metadata + +functions_table = sa.Table( + "funcapi_functions", + metadata, + sa.Column( + "uuid", + UUID(as_uuid=True), + primary_key=True, + index=True, + default=uuid.uuid4, + doc="Unique id of the function", + ), + sa.Column( + "title", + sa.String, + doc="Name of the function", + ), + sa.Column( + "function_class", + sa.String, + doc="Class of the function", + ), + sa.Column( + "description", + sa.String, + doc="Description of the function", + ), + sa.Column( + "input_schema", + sa.JSON, + doc="Input schema of the function", + ), + sa.Column( + "output_schema", + sa.JSON, + doc="Output schema of the function", + ), + sa.Column( + "system_tags", + sa.JSON, + nullable=True, + doc="System-level tags of the function", + ), + sa.Column( + "user_tags", + sa.JSON, + nullable=True, + doc="User-level tags of the function", + ), + sa.Column( + "class_specific_data", + sa.JSON, + nullable=True, + doc="Fields specific for a function class", + ), + sa.Column( + "default_inputs", + sa.JSON, + nullable=True, + doc="Default inputs of the function", + ), + sa.PrimaryKeyConstraint("uuid", name="funcapi_functions_pk"), +) diff --git a/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/functions/functions.py b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/functions/functions.py deleted file mode 100644 index d53adfafa66..00000000000 --- a/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/functions/functions.py +++ /dev/null @@ -1,22 +0,0 @@ -import logging - -from models_library.api_schemas_webserver import WEBSERVER_RPC_NAMESPACE -from models_library.rabbitmq_basic_types import RPCMethodName -from pydantic import TypeAdapter - -from .....logging_utils import log_decorator -from .....rabbitmq import RabbitMQRPCClient - -_logger = logging.getLogger(__name__) - - -@log_decorator(_logger, level=logging.DEBUG) -async def ping( - rabbitmq_rpc_client: RabbitMQRPCClient, -) -> str: - result = await rabbitmq_rpc_client.request( - WEBSERVER_RPC_NAMESPACE, - TypeAdapter(RPCMethodName).validate_python("ping"), - ) - assert isinstance(result, str) # nosec - return result diff --git a/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/functions/functions_rpc_interface.py b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/functions/functions_rpc_interface.py new file mode 100644 index 00000000000..672cf0a7657 --- /dev/null +++ b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/functions/functions_rpc_interface.py @@ -0,0 +1,293 @@ +import logging + +from models_library.api_schemas_webserver import WEBSERVER_RPC_NAMESPACE +from models_library.api_schemas_webserver.functions_wb_schema import ( + Function, + FunctionID, + FunctionInputs, + FunctionInputSchema, + FunctionJob, + FunctionJobCollection, + FunctionJobCollectionID, + FunctionJobID, + FunctionOutputSchema, + RegisteredFunction, + RegisteredFunctionJob, + RegisteredFunctionJobCollection, +) +from models_library.rabbitmq_basic_types import RPCMethodName +from models_library.rest_pagination import PageMetaInfoLimitOffset +from pydantic import TypeAdapter + +from .....logging_utils import log_decorator +from .... import RabbitMQRPCClient + +_logger = logging.getLogger(__name__) + + +@log_decorator(_logger, level=logging.DEBUG) +async def register_function( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + function: Function, +) -> RegisteredFunction: + result = await rabbitmq_rpc_client.request( + WEBSERVER_RPC_NAMESPACE, + TypeAdapter(RPCMethodName).validate_python("register_function"), + function=function, + ) + return TypeAdapter(RegisteredFunction).validate_python( + result + ) # Validates the result as a RegisteredFunction + + +@log_decorator(_logger, level=logging.DEBUG) +async def get_function( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + function_id: FunctionID, +) -> RegisteredFunction: + result = await rabbitmq_rpc_client.request( + WEBSERVER_RPC_NAMESPACE, + TypeAdapter(RPCMethodName).validate_python("get_function"), + function_id=function_id, + ) + return TypeAdapter(RegisteredFunction).validate_python(result) + + +@log_decorator(_logger, level=logging.DEBUG) +async def get_function_input_schema( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + function_id: FunctionID, +) -> FunctionInputSchema: + result = await rabbitmq_rpc_client.request( + WEBSERVER_RPC_NAMESPACE, + TypeAdapter(RPCMethodName).validate_python("get_function_input_schema"), + function_id=function_id, + ) + return TypeAdapter(FunctionInputSchema).validate_python(result) + + +@log_decorator(_logger, level=logging.DEBUG) +async def get_function_output_schema( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + function_id: FunctionID, +) -> FunctionOutputSchema: + result = await rabbitmq_rpc_client.request( + WEBSERVER_RPC_NAMESPACE, + TypeAdapter(RPCMethodName).validate_python("get_function_output_schema"), + function_id=function_id, + ) + return TypeAdapter(FunctionOutputSchema).validate_python(result) + + +@log_decorator(_logger, level=logging.DEBUG) +async def delete_function( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + function_id: FunctionID, +) -> None: + result = await rabbitmq_rpc_client.request( + WEBSERVER_RPC_NAMESPACE, + TypeAdapter(RPCMethodName).validate_python("delete_function"), + function_id=function_id, + ) + assert result is None # nosec + return result + + +@log_decorator(_logger, level=logging.DEBUG) +async def list_functions( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + pagination_limit: int, + pagination_offset: int, +) -> tuple[list[RegisteredFunction], PageMetaInfoLimitOffset]: + result: tuple[list[RegisteredFunction], PageMetaInfoLimitOffset] = ( + await rabbitmq_rpc_client.request( + WEBSERVER_RPC_NAMESPACE, + TypeAdapter(RPCMethodName).validate_python("list_functions"), + pagination_offset=pagination_offset, + pagination_limit=pagination_limit, + ) + ) + assert isinstance(result, tuple) + TypeAdapter(list[RegisteredFunction]).validate_python( + result[0] + ) # Validates the result as a list of RegisteredFunctions + assert isinstance(result[1], PageMetaInfoLimitOffset) # nosec + return result + + +@log_decorator(_logger, level=logging.DEBUG) +async def list_function_jobs( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + pagination_limit: int, + pagination_offset: int, +) -> tuple[list[RegisteredFunctionJob], PageMetaInfoLimitOffset]: + result: tuple[list[RegisteredFunctionJob], PageMetaInfoLimitOffset] = ( + await rabbitmq_rpc_client.request( + WEBSERVER_RPC_NAMESPACE, + TypeAdapter(RPCMethodName).validate_python("list_function_jobs"), + pagination_offset=pagination_offset, + pagination_limit=pagination_limit, + ) + ) + assert isinstance(result, tuple) # nosec + assert isinstance(result[0], list) # nosec + assert all( + TypeAdapter(RegisteredFunctionJob).validate_python(item) for item in result[0] + ) # nosec + assert isinstance(result[1], PageMetaInfoLimitOffset) # nosec + return ( + TypeAdapter(list[RegisteredFunctionJob]).validate_python(result[0]), + TypeAdapter(PageMetaInfoLimitOffset).validate_python(result[1]), + ) + + +@log_decorator(_logger, level=logging.DEBUG) +async def list_function_job_collections( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + pagination_limit: int, + pagination_offset: int, +) -> tuple[list[RegisteredFunctionJobCollection], PageMetaInfoLimitOffset]: + result = await rabbitmq_rpc_client.request( + WEBSERVER_RPC_NAMESPACE, + TypeAdapter(RPCMethodName).validate_python("list_function_job_collections"), + pagination_offset=pagination_offset, + pagination_limit=pagination_limit, + ) + assert isinstance(result, tuple) + return ( + TypeAdapter(list[RegisteredFunctionJobCollection]).validate_python( + result[0] + ), # Validates the result as a list of RegisteredFunctionJobCollections + TypeAdapter(PageMetaInfoLimitOffset).validate_python(result[1]), # nosec + ) + + +@log_decorator(_logger, level=logging.DEBUG) +async def run_function( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + function_id: FunctionID, + inputs: FunctionInputs, +) -> RegisteredFunctionJob: + result = await rabbitmq_rpc_client.request( + WEBSERVER_RPC_NAMESPACE, + TypeAdapter(RPCMethodName).validate_python("run_function"), + function_id=function_id, + inputs=inputs, + ) + return TypeAdapter(RegisteredFunctionJob).validate_python( + result + ) # Validates the result as a RegisteredFunctionJob + + +@log_decorator(_logger, level=logging.DEBUG) +async def register_function_job( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + function_job: FunctionJob, +) -> RegisteredFunctionJob: + result = await rabbitmq_rpc_client.request( + WEBSERVER_RPC_NAMESPACE, + TypeAdapter(RPCMethodName).validate_python("register_function_job"), + function_job=function_job, + ) + return TypeAdapter(RegisteredFunctionJob).validate_python( + result + ) # Validates the result as a RegisteredFunctionJob + + +@log_decorator(_logger, level=logging.DEBUG) +async def get_function_job( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + function_job_id: FunctionJobID, +) -> RegisteredFunctionJob: + result = await rabbitmq_rpc_client.request( + WEBSERVER_RPC_NAMESPACE, + TypeAdapter(RPCMethodName).validate_python("get_function_job"), + function_job_id=function_job_id, + ) + + return TypeAdapter(RegisteredFunctionJob).validate_python(result) + + +@log_decorator(_logger, level=logging.DEBUG) +async def delete_function_job( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + function_job_id: FunctionJobID, +) -> None: + result: None = await rabbitmq_rpc_client.request( + WEBSERVER_RPC_NAMESPACE, + TypeAdapter(RPCMethodName).validate_python("delete_function_job"), + function_job_id=function_job_id, + ) + assert result is None # nosec + + +@log_decorator(_logger, level=logging.DEBUG) +async def find_cached_function_job( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + function_id: FunctionID, + inputs: FunctionInputs, +) -> RegisteredFunctionJob | None: + result = await rabbitmq_rpc_client.request( + WEBSERVER_RPC_NAMESPACE, + TypeAdapter(RPCMethodName).validate_python("find_cached_function_job"), + function_id=function_id, + inputs=inputs, + ) + if result is None: + return None + return TypeAdapter(RegisteredFunctionJob).validate_python(result) + + +@log_decorator(_logger, level=logging.DEBUG) +async def register_function_job_collection( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + function_job_collection: FunctionJobCollection, +) -> RegisteredFunctionJobCollection: + result = await rabbitmq_rpc_client.request( + WEBSERVER_RPC_NAMESPACE, + TypeAdapter(RPCMethodName).validate_python("register_function_job_collection"), + function_job_collection=function_job_collection, + ) + return TypeAdapter(RegisteredFunctionJobCollection).validate_python(result) + + +@log_decorator(_logger, level=logging.DEBUG) +async def get_function_job_collection( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + function_job_collection_id: FunctionJobCollectionID, +) -> RegisteredFunctionJobCollection: + result = await rabbitmq_rpc_client.request( + WEBSERVER_RPC_NAMESPACE, + TypeAdapter(RPCMethodName).validate_python("get_function_job_collection"), + function_job_collection_id=function_job_collection_id, + ) + return TypeAdapter(RegisteredFunctionJobCollection).validate_python(result) + + +@log_decorator(_logger, level=logging.DEBUG) +async def delete_function_job_collection( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + function_job_collection_id: FunctionJobCollectionID, +) -> None: + result = await rabbitmq_rpc_client.request( + WEBSERVER_RPC_NAMESPACE, + TypeAdapter(RPCMethodName).validate_python("delete_function_job_collection"), + function_job_collection_id=function_job_collection_id, + ) + assert result is None # nosec diff --git a/services/api-server/openapi.json b/services/api-server/openapi.json index e7ff27a22a1..b2ee76f8869 100644 --- a/services/api-server/openapi.json +++ b/services/api-server/openapi.json @@ -5276,27 +5276,75 @@ } } }, - "/v0/wallets/default": { - "get": { + "/v0/functions": { + "post": { "tags": [ - "wallets" + "functions" ], - "summary": "Get Default Wallet", - "description": "Get default wallet\n\nNew in *version 0.7*", - "operationId": "get_default_wallet", + "summary": "Register Function", + "description": "Create function", + "operationId": "register_function", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProjectFunction" + }, + { + "$ref": "#/components/schemas/PythonCodeFunction" + }, + { + "$ref": "#/components/schemas/SolverFunction" + } + ], + "discriminator": { + "propertyName": "function_class", + "mapping": { + "project": "#/components/schemas/ProjectFunction", + "python_code": "#/components/schemas/PythonCodeFunction", + "solver": "#/components/schemas/SolverFunction" + } + }, + "title": "Function" + } + } + } + }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/WalletGetWithAvailableCreditsLegacy" + "oneOf": [ + { + "$ref": "#/components/schemas/RegisteredProjectFunction" + }, + { + "$ref": "#/components/schemas/RegisteredPythonCodeFunction" + }, + { + "$ref": "#/components/schemas/RegisteredSolverFunction" + } + ], + "discriminator": { + "propertyName": "function_class", + "mapping": { + "project": "#/components/schemas/RegisteredProjectFunction", + "python_code": "#/components/schemas/RegisteredPythonCodeFunction", + "solver": "#/components/schemas/RegisteredSolverFunction" + } + }, + "title": "Response Register Function V0 Functions Post" } } } }, "404": { - "description": "Wallet not found", + "description": "Function not found", "content": { "application/json": { "schema": { @@ -5305,48 +5353,126 @@ } } }, - "403": { - "description": "Access to wallet is not allowed", + "422": { + "description": "Validation Error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorGet" + "$ref": "#/components/schemas/HTTPValidationError" } } } + } + } + }, + "get": { + "tags": [ + "functions" + ], + "summary": "List Functions", + "description": "List functions", + "operationId": "list_functions", + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 50, + "minimum": 1, + "default": 20, + "title": "Limit" + } }, - "429": { - "description": "Too many requests", + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0, + "title": "Offset" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorGet" + "$ref": "#/components/schemas/Page_Annotated_Union_RegisteredProjectFunction__RegisteredPythonCodeFunction__RegisteredSolverFunction___FieldInfo_annotation_NoneType__required_True__discriminator__function_class____" } } } }, - "500": { - "description": "Internal server error", + "422": { + "description": "Validation Error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorGet" + "$ref": "#/components/schemas/HTTPValidationError" } } } - }, - "502": { - "description": "Unexpected error when communicating with backend service", + } + } + } + }, + "/v0/functions/{function_id}": { + "get": { + "tags": [ + "functions" + ], + "summary": "Get Function", + "description": "Get function", + "operationId": "get_function", + "parameters": [ + { + "name": "function_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Function Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorGet" + "oneOf": [ + { + "$ref": "#/components/schemas/RegisteredProjectFunction" + }, + { + "$ref": "#/components/schemas/RegisteredPythonCodeFunction" + }, + { + "$ref": "#/components/schemas/RegisteredSolverFunction" + } + ], + "discriminator": { + "propertyName": "function_class", + "mapping": { + "project": "#/components/schemas/RegisteredProjectFunction", + "python_code": "#/components/schemas/RegisteredPythonCodeFunction", + "solver": "#/components/schemas/RegisteredSolverFunction" + } + }, + "title": "Response Get Function V0 Functions Function Id Get" } } } }, - "503": { - "description": "Service unavailable", + "404": { + "description": "Function not found", "content": { "application/json": { "schema": { @@ -5355,45 +5481,34 @@ } } }, - "504": { - "description": "Request to a backend service timed out.", + "422": { + "description": "Validation Error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorGet" + "$ref": "#/components/schemas/HTTPValidationError" } } } } - }, - "security": [ - { - "HTTPBasic": [] - } - ] - } - }, - "/v0/wallets/{wallet_id}": { - "get": { + } + }, + "delete": { "tags": [ - "wallets" - ], - "summary": "Get Wallet", - "description": "Get wallet\n\nNew in *version 0.7*", - "operationId": "get_wallet", - "security": [ - { - "HTTPBasic": [] - } + "functions" ], + "summary": "Delete Function", + "description": "Delete function", + "operationId": "delete_function", "parameters": [ { - "name": "wallet_id", + "name": "function_id", "in": "path", "required": true, "schema": { - "type": "integer", - "title": "Wallet Id" + "type": "string", + "format": "uuid", + "title": "Function Id" } } ], @@ -5402,14 +5517,12 @@ "description": "Successful Response", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/WalletGetWithAvailableCreditsLegacy" - } + "schema": {} } } }, "404": { - "description": "Wallet not found", + "description": "Function not found", "content": { "application/json": { "schema": { @@ -5418,28 +5531,63 @@ } } }, - "403": { - "description": "Access to wallet is not allowed", + "422": { + "description": "Validation Error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorGet" + "$ref": "#/components/schemas/HTTPValidationError" } } } - }, - "429": { - "description": "Too many requests", + } + } + } + }, + "/v0/functions/{function_id}/input_schema": { + "get": { + "tags": [ + "functions" + ], + "summary": "Get Function Inputschema", + "description": "Get function input schema", + "operationId": "get_function_inputschema", + "parameters": [ + { + "name": "function_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Function Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorGet" + "oneOf": [ + { + "$ref": "#/components/schemas/JSONFunctionInputSchema" + } + ], + "discriminator": { + "propertyName": "schema_class", + "mapping": { + "application/schema+json": "#/components/schemas/JSONFunctionInputSchema" + } + }, + "title": "Response Get Function Inputschema V0 Functions Function Id Input Schema Get" } } } }, - "500": { - "description": "Internal server error", + "404": { + "description": "Function not found", "content": { "application/json": { "schema": { @@ -5448,28 +5596,63 @@ } } }, - "502": { - "description": "Unexpected error when communicating with backend service", + "422": { + "description": "Validation Error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorGet" + "$ref": "#/components/schemas/HTTPValidationError" } } } - }, - "503": { - "description": "Service unavailable", + } + } + } + }, + "/v0/functions/{function_id}/output_schema": { + "get": { + "tags": [ + "functions" + ], + "summary": "Get Function Outputschema", + "description": "Get function input schema", + "operationId": "get_function_outputschema", + "parameters": [ + { + "name": "function_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Function Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorGet" + "oneOf": [ + { + "$ref": "#/components/schemas/JSONFunctionInputSchema" + } + ], + "discriminator": { + "propertyName": "schema_class", + "mapping": { + "application/schema+json": "#/components/schemas/JSONFunctionInputSchema" + } + }, + "title": "Response Get Function Outputschema V0 Functions Function Id Output Schema Get" } } } }, - "504": { - "description": "Request to a backend service timed out.", + "404": { + "description": "Function not found", "content": { "application/json": { "schema": { @@ -5491,126 +5674,160 @@ } } }, - "/v0/wallets/{wallet_id}/licensed-items": { - "get": { + "/v0/functions/{function_id}:validate_inputs": { + "post": { "tags": [ - "wallets" - ], - "summary": "Get Available Licensed Items For Wallet", - "description": "Get all available licensed items for a given wallet", - "operationId": "get_available_licensed_items_for_wallet", - "security": [ - { - "HTTPBasic": [] - } + "functions" ], + "summary": "Validate Function Inputs", + "description": "Validate inputs against the function's input schema", + "operationId": "validate_function_inputs", "parameters": [ { - "name": "wallet_id", + "name": "function_id", "in": "path", "required": true, "schema": { - "type": "integer", - "title": "Wallet Id" - } - }, - { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "maximum": 50, - "minimum": 1, - "default": 20, - "title": "Limit" - } - }, - { - "name": "offset", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "minimum": 0, - "default": 0, - "title": "Offset" + "type": "string", + "format": "uuid", + "title": "Function Id" } } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Inputs" + } + } + } + }, "responses": { "200": { "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Page_LicensedItemGet_" + "type": "array", + "prefixItems": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ], + "minItems": 2, + "maxItems": 2, + "title": "Response Validate Function Inputs V0 Functions Function Id Validate Inputs Post" } } } }, - "404": { - "description": "Wallet not found", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorGet" - } - } - } + "400": { + "description": "Invalid inputs" }, - "403": { - "description": "Access to wallet is not allowed", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorGet" - } - } - } + "404": { + "description": "Function not found" }, - "429": { - "description": "Too many requests", + "422": { + "description": "Validation Error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorGet" + "$ref": "#/components/schemas/HTTPValidationError" } } } - }, - "500": { - "description": "Internal server error", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorGet" - } - } + } + } + } + }, + "/v0/functions/{function_id}:run": { + "post": { + "tags": [ + "functions" + ], + "summary": "Run Function", + "description": "Run function", + "operationId": "run_function", + "security": [ + { + "HTTPBasic": [] + } + ], + "parameters": [ + { + "name": "function_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Function Id" } - }, - "502": { - "description": "Unexpected error when communicating with backend service", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorGet" - } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Function Inputs" } } - }, - "503": { - "description": "Service unavailable", + } + }, + "responses": { + "200": { + "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorGet" + "oneOf": [ + { + "$ref": "#/components/schemas/RegisteredProjectFunctionJob" + }, + { + "$ref": "#/components/schemas/RegisteredPythonCodeFunctionJob" + }, + { + "$ref": "#/components/schemas/RegisteredSolverFunctionJob" + } + ], + "discriminator": { + "propertyName": "function_class", + "mapping": { + "project": "#/components/schemas/RegisteredProjectFunctionJob", + "python_code": "#/components/schemas/RegisteredPythonCodeFunctionJob", + "solver": "#/components/schemas/RegisteredSolverFunctionJob" + } + }, + "title": "Response Run Function V0 Functions Function Id Run Post" } } } }, - "504": { - "description": "Request to a backend service timed out.", + "404": { + "description": "Function not found", "content": { "application/json": { "schema": { @@ -5632,14 +5849,14 @@ } } }, - "/v0/wallets/{wallet_id}/licensed-items/{licensed_item_id}/checkout": { + "/v0/functions/{function_id}:map": { "post": { "tags": [ - "wallets" + "functions" ], - "summary": "Checkout Licensed Item", - "description": "Checkout licensed item", - "operationId": "checkout_licensed_item", + "summary": "Map Function", + "description": "Map function over input parameters", + "operationId": "map_function", "security": [ { "HTTPBasic": [] @@ -5647,22 +5864,13 @@ ], "parameters": [ { - "name": "wallet_id", - "in": "path", - "required": true, - "schema": { - "type": "integer", - "title": "Wallet Id" - } - }, - { - "name": "licensed_item_id", + "name": "function_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid", - "title": "Licensed Item Id" + "title": "Function Id" } } ], @@ -5671,7 +5879,18 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LicensedItemCheckoutData" + "type": "array", + "items": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ] + }, + "title": "Function Inputs List" } } } @@ -5682,13 +5901,13 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LicensedItemCheckoutGet" + "$ref": "#/components/schemas/RegisteredFunctionJobCollection" } } } }, "404": { - "description": "Wallet not found", + "description": "Function not found", "content": { "application/json": { "schema": { @@ -5697,141 +5916,242 @@ } } }, - "403": { - "description": "Access to wallet is not allowed", + "422": { + "description": "Validation Error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorGet" + "$ref": "#/components/schemas/HTTPValidationError" } } } + } + } + } + }, + "/v0/function_jobs": { + "get": { + "tags": [ + "function_jobs" + ], + "summary": "List Function Jobs", + "description": "List function jobs", + "operationId": "list_function_jobs", + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 50, + "minimum": 1, + "default": 20, + "title": "Limit" + } }, - "429": { - "description": "Too many requests", + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0, + "title": "Offset" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorGet" + "$ref": "#/components/schemas/Page_Annotated_Union_RegisteredProjectFunctionJob__RegisteredPythonCodeFunctionJob__RegisteredSolverFunctionJob___FieldInfo_annotation_NoneType__required_True__discriminator__function_class____" } } } }, - "500": { - "description": "Internal server error", + "422": { + "description": "Validation Error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorGet" + "$ref": "#/components/schemas/HTTPValidationError" } } } - }, - "502": { - "description": "Unexpected error when communicating with backend service", + } + } + }, + "post": { + "tags": [ + "function_jobs" + ], + "summary": "Register Function Job", + "description": "Create function job", + "operationId": "register_function_job", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/ProjectFunctionJob" + }, + { + "$ref": "#/components/schemas/PythonCodeFunctionJob" + }, + { + "$ref": "#/components/schemas/SolverFunctionJob" + } + ], + "discriminator": { + "propertyName": "function_class", + "mapping": { + "project": "#/components/schemas/ProjectFunctionJob", + "python_code": "#/components/schemas/PythonCodeFunctionJob", + "solver": "#/components/schemas/SolverFunctionJob" + } + }, + "title": "Function Job" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorGet" + "oneOf": [ + { + "$ref": "#/components/schemas/RegisteredProjectFunctionJob" + }, + { + "$ref": "#/components/schemas/RegisteredPythonCodeFunctionJob" + }, + { + "$ref": "#/components/schemas/RegisteredSolverFunctionJob" + } + ], + "discriminator": { + "propertyName": "function_class", + "mapping": { + "project": "#/components/schemas/RegisteredProjectFunctionJob", + "python_code": "#/components/schemas/RegisteredPythonCodeFunctionJob", + "solver": "#/components/schemas/RegisteredSolverFunctionJob" + } + }, + "title": "Response Register Function Job V0 Function Jobs Post" } } } }, - "503": { - "description": "Service unavailable", + "422": { + "description": "Validation Error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorGet" + "$ref": "#/components/schemas/HTTPValidationError" } } } - }, - "504": { - "description": "Request to a backend service timed out.", + } + } + } + }, + "/v0/function_jobs/{function_job_id}": { + "get": { + "tags": [ + "function_jobs" + ], + "summary": "Get Function Job", + "description": "Get function job", + "operationId": "get_function_job", + "parameters": [ + { + "name": "function_job_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Function Job Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorGet" + "oneOf": [ + { + "$ref": "#/components/schemas/RegisteredProjectFunctionJob" + }, + { + "$ref": "#/components/schemas/RegisteredPythonCodeFunctionJob" + }, + { + "$ref": "#/components/schemas/RegisteredSolverFunctionJob" + } + ], + "discriminator": { + "propertyName": "function_class", + "mapping": { + "project": "#/components/schemas/RegisteredProjectFunctionJob", + "python_code": "#/components/schemas/RegisteredPythonCodeFunctionJob", + "solver": "#/components/schemas/RegisteredSolverFunctionJob" + } + }, + "title": "Response Get Function Job V0 Function Jobs Function Job Id Get" } } } }, - "422": { - "description": "Validation Error", + "404": { + "description": "Function job not found", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/HTTPValidationError" + "$ref": "#/components/schemas/ErrorGet" } } } - } - } - } - }, - "/v0/credits/price": { - "get": { - "tags": [ - "credits" - ], - "summary": "Get Credits Price", - "description": "New in *version 0.6.0*", - "operationId": "get_credits_price", - "responses": { - "200": { - "description": "Successful Response", + }, + "422": { + "description": "Validation Error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/GetCreditPriceLegacy" + "$ref": "#/components/schemas/HTTPValidationError" } } } } - }, - "security": [ - { - "HTTPBasic": [] - } - ] - } - }, - "/v0/licensed-items": { - "get": { + } + }, + "delete": { "tags": [ - "licensed-items" - ], - "summary": "Get Licensed Items", - "description": "Get all licensed items", - "operationId": "get_licensed_items", - "security": [ - { - "HTTPBasic": [] - } + "function_jobs" ], + "summary": "Delete Function Job", + "description": "Delete function job", + "operationId": "delete_function_job", "parameters": [ { - "name": "limit", - "in": "query", - "required": false, - "schema": { - "type": "integer", - "maximum": 50, - "minimum": 1, - "default": 20, - "title": "Limit" - } - }, - { - "name": "offset", - "in": "query", - "required": false, + "name": "function_job_id", + "in": "path", + "required": true, "schema": { - "type": "integer", - "minimum": 0, - "default": 0, - "title": "Offset" + "type": "string", + "format": "uuid", + "title": "Function Job Id" } } ], @@ -5840,14 +6160,12 @@ "description": "Successful Response", "content": { "application/json": { - "schema": { - "$ref": "#/components/schemas/Page_LicensedItemGet_" - } + "schema": {} } } }, - "429": { - "description": "Too many requests", + "404": { + "description": "Function job not found", "content": { "application/json": { "schema": { @@ -5856,38 +6174,57 @@ } } }, - "500": { - "description": "Internal server error", + "422": { + "description": "Validation Error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorGet" + "$ref": "#/components/schemas/HTTPValidationError" } } } - }, - "502": { - "description": "Unexpected error when communicating with backend service", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ErrorGet" - } - } + } + } + } + }, + "/v0/function_jobs/{function_job_id}/status": { + "get": { + "tags": [ + "function_jobs" + ], + "summary": "Function Job Status", + "description": "Get function job status", + "operationId": "function_job_status", + "security": [ + { + "HTTPBasic": [] + } + ], + "parameters": [ + { + "name": "function_job_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Function Job Id" } - }, - "503": { - "description": "Service unavailable", + } + ], + "responses": { + "200": { + "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorGet" + "$ref": "#/components/schemas/FunctionJobStatus" } } } }, - "504": { - "description": "Request to a backend service timed out.", + "404": { + "description": "Function job not found", "content": { "application/json": { "schema": { @@ -5909,14 +6246,14 @@ } } }, - "/v0/licensed-items/{licensed_item_id}/checked-out-items/{licensed_item_checkout_id}/release": { - "post": { + "/v0/function_jobs/{function_job_id}/outputs": { + "get": { "tags": [ - "licensed-items" + "function_jobs" ], - "summary": "Release Licensed Item", - "description": "Release previously checked out licensed item", - "operationId": "release_licensed_item", + "summary": "Function Job Outputs", + "description": "Get function job outputs", + "operationId": "function_job_outputs", "security": [ { "HTTPBasic": [] @@ -5924,23 +6261,13 @@ ], "parameters": [ { - "name": "licensed_item_id", - "in": "path", - "required": true, - "schema": { - "type": "string", - "format": "uuid", - "title": "Licensed Item Id" - } - }, - { - "name": "licensed_item_checkout_id", + "name": "function_job_id", "in": "path", "required": true, "schema": { "type": "string", "format": "uuid", - "title": "Licensed Item Checkout Id" + "title": "Function Job Id" } } ], @@ -5950,13 +6277,21 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/LicensedItemCheckoutGet" + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Response Function Job Outputs V0 Function Jobs Function Job Id Outputs Get" } } } }, - "429": { - "description": "Too many requests", + "404": { + "description": "Function job not found", "content": { "application/json": { "schema": { @@ -5965,42 +6300,1080 @@ } } }, - "500": { - "description": "Internal server error", + "422": { + "description": "Validation Error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorGet" + "$ref": "#/components/schemas/HTTPValidationError" } } } + } + } + } + }, + "/v0/function_job_collections": { + "get": { + "tags": [ + "function_job_collections" + ], + "summary": "List Function Job Collections", + "description": "List function job collections", + "operationId": "list_function_job_collections", + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 50, + "minimum": 1, + "default": 20, + "title": "Limit" + } }, - "502": { - "description": "Unexpected error when communicating with backend service", + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0, + "title": "Offset" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorGet" + "$ref": "#/components/schemas/Page_RegisteredFunctionJobCollection_" } } } }, - "503": { - "description": "Service unavailable", + "422": { + "description": "Validation Error", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorGet" + "$ref": "#/components/schemas/HTTPValidationError" } } } - }, - "504": { - "description": "Request to a backend service timed out.", + } + } + }, + "post": { + "tags": [ + "function_job_collections" + ], + "summary": "Register Function Job Collection", + "description": "Register function job collection", + "operationId": "register_function_job_collection", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FunctionJobCollection" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ErrorGet" + "$ref": "#/components/schemas/RegisteredFunctionJobCollection" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v0/function_job_collections/{function_job_collection_id}": { + "get": { + "tags": [ + "function_job_collections" + ], + "summary": "Get Function Job Collection", + "description": "Get function job collection", + "operationId": "get_function_job_collection", + "parameters": [ + { + "name": "function_job_collection_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Function Job Collection Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RegisteredFunctionJobCollection" + } + } + } + }, + "404": { + "description": "Function job collection not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": [ + "function_job_collections" + ], + "summary": "Delete Function Job Collection", + "description": "Delete function job collection", + "operationId": "delete_function_job_collection", + "parameters": [ + { + "name": "function_job_collection_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Function Job Collection Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": {} + } + } + }, + "404": { + "description": "Function job collection not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v0/function_job_collections/{function_job_collection_id}/function_jobs": { + "get": { + "tags": [ + "function_job_collections" + ], + "summary": "Function Job Collection List Function Jobs", + "description": "Get the function jobs in function job collection", + "operationId": "function_job_collection_list_function_jobs", + "parameters": [ + { + "name": "function_job_collection_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Function Job Collection Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/RegisteredProjectFunctionJob" + }, + { + "$ref": "#/components/schemas/RegisteredPythonCodeFunctionJob" + }, + { + "$ref": "#/components/schemas/RegisteredSolverFunctionJob" + } + ], + "discriminator": { + "propertyName": "function_class", + "mapping": { + "project": "#/components/schemas/RegisteredProjectFunctionJob", + "python_code": "#/components/schemas/RegisteredPythonCodeFunctionJob", + "solver": "#/components/schemas/RegisteredSolverFunctionJob" + } + } + }, + "title": "Response Function Job Collection List Function Jobs V0 Function Job Collections Function Job Collection Id Function Jobs Get" + } + } + } + }, + "404": { + "description": "Function job collection not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v0/function_job_collections/{function_job_collection_id}/status": { + "get": { + "tags": [ + "function_job_collections" + ], + "summary": "Function Job Collection Status", + "description": "Get function job collection status", + "operationId": "function_job_collection_status", + "security": [ + { + "HTTPBasic": [] + } + ], + "parameters": [ + { + "name": "function_job_collection_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Function Job Collection Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FunctionJobCollectionStatus" + } + } + } + }, + "404": { + "description": "Function job collection not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v0/wallets/default": { + "get": { + "tags": [ + "wallets" + ], + "summary": "Get Default Wallet", + "description": "Get default wallet\n\nNew in *version 0.7*", + "operationId": "get_default_wallet", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WalletGetWithAvailableCreditsLegacy" + } + } + } + }, + "404": { + "description": "Wallet not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "403": { + "description": "Access to wallet is not allowed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "429": { + "description": "Too many requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "502": { + "description": "Unexpected error when communicating with backend service", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "503": { + "description": "Service unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "504": { + "description": "Request to a backend service timed out.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + } + }, + "security": [ + { + "HTTPBasic": [] + } + ] + } + }, + "/v0/wallets/{wallet_id}": { + "get": { + "tags": [ + "wallets" + ], + "summary": "Get Wallet", + "description": "Get wallet\n\nNew in *version 0.7*", + "operationId": "get_wallet", + "security": [ + { + "HTTPBasic": [] + } + ], + "parameters": [ + { + "name": "wallet_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Wallet Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/WalletGetWithAvailableCreditsLegacy" + } + } + } + }, + "404": { + "description": "Wallet not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "403": { + "description": "Access to wallet is not allowed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "429": { + "description": "Too many requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "502": { + "description": "Unexpected error when communicating with backend service", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "503": { + "description": "Service unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "504": { + "description": "Request to a backend service timed out.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v0/wallets/{wallet_id}/licensed-items": { + "get": { + "tags": [ + "wallets" + ], + "summary": "Get Available Licensed Items For Wallet", + "description": "Get all available licensed items for a given wallet", + "operationId": "get_available_licensed_items_for_wallet", + "security": [ + { + "HTTPBasic": [] + } + ], + "parameters": [ + { + "name": "wallet_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Wallet Id" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 50, + "minimum": 1, + "default": 20, + "title": "Limit" + } + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0, + "title": "Offset" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Page_LicensedItemGet_" + } + } + } + }, + "404": { + "description": "Wallet not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "403": { + "description": "Access to wallet is not allowed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "429": { + "description": "Too many requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "502": { + "description": "Unexpected error when communicating with backend service", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "503": { + "description": "Service unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "504": { + "description": "Request to a backend service timed out.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v0/wallets/{wallet_id}/licensed-items/{licensed_item_id}/checkout": { + "post": { + "tags": [ + "wallets" + ], + "summary": "Checkout Licensed Item", + "description": "Checkout licensed item", + "operationId": "checkout_licensed_item", + "security": [ + { + "HTTPBasic": [] + } + ], + "parameters": [ + { + "name": "wallet_id", + "in": "path", + "required": true, + "schema": { + "type": "integer", + "title": "Wallet Id" + } + }, + { + "name": "licensed_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Licensed Item Id" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LicensedItemCheckoutData" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LicensedItemCheckoutGet" + } + } + } + }, + "404": { + "description": "Wallet not found", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "403": { + "description": "Access to wallet is not allowed", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "429": { + "description": "Too many requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "502": { + "description": "Unexpected error when communicating with backend service", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "503": { + "description": "Service unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "504": { + "description": "Request to a backend service timed out.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v0/credits/price": { + "get": { + "tags": [ + "credits" + ], + "summary": "Get Credits Price", + "description": "New in *version 0.6.0*", + "operationId": "get_credits_price", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetCreditPriceLegacy" + } + } + } + } + }, + "security": [ + { + "HTTPBasic": [] + } + ] + } + }, + "/v0/licensed-items": { + "get": { + "tags": [ + "licensed-items" + ], + "summary": "Get Licensed Items", + "description": "Get all licensed items", + "operationId": "get_licensed_items", + "security": [ + { + "HTTPBasic": [] + } + ], + "parameters": [ + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "maximum": 50, + "minimum": 1, + "default": 20, + "title": "Limit" + } + }, + { + "name": "offset", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 0, + "default": 0, + "title": "Offset" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Page_LicensedItemGet_" + } + } + } + }, + "429": { + "description": "Too many requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "502": { + "description": "Unexpected error when communicating with backend service", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "503": { + "description": "Service unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "504": { + "description": "Request to a backend service timed out.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/v0/licensed-items/{licensed_item_id}/checked-out-items/{licensed_item_checkout_id}/release": { + "post": { + "tags": [ + "licensed-items" + ], + "summary": "Release Licensed Item", + "description": "Release previously checked out licensed item", + "operationId": "release_licensed_item", + "security": [ + { + "HTTPBasic": [] + } + ], + "parameters": [ + { + "name": "licensed_item_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Licensed Item Id" + } + }, + { + "name": "licensed_item_checkout_id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid", + "title": "Licensed Item Checkout Id" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LicensedItemCheckoutGet" + } + } + } + }, + "429": { + "description": "Too many requests", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "502": { + "description": "Unexpected error when communicating with backend service", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "503": { + "description": "Service unavailable", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" + } + } + } + }, + "504": { + "description": "Request to a backend service timed out.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorGet" } } } @@ -6023,146 +7396,1200 @@ "schemas": { "Body_abort_multipart_upload_v0_files__file_id__abort_post": { "properties": { - "client_file": { + "client_file": { + "anyOf": [ + { + "$ref": "#/components/schemas/UserFileToProgramJob" + }, + { + "$ref": "#/components/schemas/UserFile" + } + ], + "title": "Client File" + } + }, + "type": "object", + "required": [ + "client_file" + ], + "title": "Body_abort_multipart_upload_v0_files__file_id__abort_post" + }, + "Body_complete_multipart_upload_v0_files__file_id__complete_post": { + "properties": { + "client_file": { + "anyOf": [ + { + "$ref": "#/components/schemas/UserFileToProgramJob" + }, + { + "$ref": "#/components/schemas/UserFile" + } + ], + "title": "Client File" + }, + "uploaded_parts": { + "$ref": "#/components/schemas/FileUploadCompletionBody" + } + }, + "type": "object", + "required": [ + "client_file", + "uploaded_parts" + ], + "title": "Body_complete_multipart_upload_v0_files__file_id__complete_post" + }, + "Body_create_program_job_v0_programs__program_key__releases__version__jobs_post": { + "properties": { + "name": { + "anyOf": [ + { + "type": "string", + "maxLength": 500 + }, + { + "type": "null" + } + ], + "title": "Name" + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 500 + }, + { + "type": "null" + } + ], + "title": "Description" + } + }, + "type": "object", + "title": "Body_create_program_job_v0_programs__program_key__releases__version__jobs_post" + }, + "Body_upload_file_v0_files_content_put": { + "properties": { + "file": { + "type": "string", + "format": "binary", + "title": "File" + } + }, + "type": "object", + "required": [ + "file" + ], + "title": "Body_upload_file_v0_files_content_put" + }, + "ClientFileUploadData": { + "properties": { + "file_id": { + "type": "string", + "format": "uuid", + "title": "File Id", + "description": "The file resource id" + }, + "upload_schema": { + "$ref": "#/components/schemas/FileUploadData", + "description": "Schema for uploading file" + } + }, + "type": "object", + "required": [ + "file_id", + "upload_schema" + ], + "title": "ClientFileUploadData" + }, + "ErrorGet": { + "properties": { + "errors": { + "items": {}, + "type": "array", + "title": "Errors" + } + }, + "type": "object", + "required": [ + "errors" + ], + "title": "ErrorGet", + "example": { + "errors": [ + "some error message", + "another error message" + ] + } + }, + "File": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id", + "description": "Resource identifier" + }, + "filename": { + "type": "string", + "title": "Filename", + "description": "Name of the file with extension" + }, + "content_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Content Type", + "description": "Guess of type content [EXPERIMENTAL]" + }, + "checksum": { + "anyOf": [ + { + "type": "string", + "pattern": "^[a-fA-F0-9]{64}$" + }, + { + "type": "null" + } + ], + "title": "Checksum", + "description": "SHA256 hash of the file's content" + }, + "e_tag": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "E Tag", + "description": "S3 entity tag" + } + }, + "type": "object", + "required": [ + "id", + "filename" + ], + "title": "File", + "description": "Represents a file stored on the server side i.e. a unique reference to a file in the cloud." + }, + "FileUploadCompletionBody": { + "properties": { + "parts": { + "items": { + "$ref": "#/components/schemas/UploadedPart" + }, + "type": "array", + "title": "Parts" + } + }, + "type": "object", + "required": [ + "parts" + ], + "title": "FileUploadCompletionBody" + }, + "FileUploadData": { + "properties": { + "chunk_size": { + "type": "integer", + "minimum": 0, + "title": "Chunk Size" + }, + "urls": { + "items": { + "type": "string", + "minLength": 1, + "format": "uri" + }, + "type": "array", + "title": "Urls" + }, + "links": { + "$ref": "#/components/schemas/UploadLinks" + } + }, + "type": "object", + "required": [ + "chunk_size", + "urls", + "links" + ], + "title": "FileUploadData" + }, + "FunctionJobCollection": { + "properties": { + "title": { + "type": "string", + "title": "Title", + "default": "" + }, + "description": { + "type": "string", + "title": "Description", + "default": "" + }, + "job_ids": { + "items": { + "type": "string", + "format": "uuid" + }, + "type": "array", + "title": "Job Ids", + "default": [] + } + }, + "type": "object", + "title": "FunctionJobCollection", + "description": "Model for a collection of function jobs" + }, + "FunctionJobCollectionStatus": { + "properties": { + "status": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Status" + } + }, + "type": "object", + "required": [ + "status" + ], + "title": "FunctionJobCollectionStatus" + }, + "FunctionJobStatus": { + "properties": { + "status": { + "type": "string", + "title": "Status" + } + }, + "type": "object", + "required": [ + "status" + ], + "title": "FunctionJobStatus" + }, + "GetCreditPriceLegacy": { + "properties": { + "productName": { + "type": "string", + "title": "Productname" + }, + "usdPerCredit": { + "anyOf": [ + { + "type": "number", + "minimum": 0.0 + }, + { + "type": "null" + } + ], + "title": "Usdpercredit", + "description": "Price of a credit in USD. If None, then this product's price is UNDEFINED" + }, + "minPaymentAmountUsd": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ], + "title": "Minpaymentamountusd", + "description": "Minimum amount (included) in USD that can be paid for this productCan be None if this product's price is UNDEFINED" + } + }, + "type": "object", + "required": [ + "productName", + "usdPerCredit", + "minPaymentAmountUsd" + ], + "title": "GetCreditPriceLegacy" + }, + "Groups": { + "properties": { + "me": { + "$ref": "#/components/schemas/UsersGroup" + }, + "organizations": { + "anyOf": [ + { + "items": { + "$ref": "#/components/schemas/UsersGroup" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "title": "Organizations", + "default": [] + }, + "all": { + "$ref": "#/components/schemas/UsersGroup" + } + }, + "type": "object", + "required": [ + "me", + "all" + ], + "title": "Groups" + }, + "HTTPValidationError": { + "properties": { + "errors": { + "items": { + "$ref": "#/components/schemas/ValidationError" + }, + "type": "array", + "title": "Validation errors" + } + }, + "type": "object", + "title": "HTTPValidationError" + }, + "JSONFunctionInputSchema": { + "properties": { + "schema_content": { + "type": "object", + "title": "JSON Schema", + "description": "JSON Schema", + "default": {} + }, + "schema_class": { + "type": "string", + "const": "application/schema+json", + "title": "Schema Class", + "default": "application/schema+json" + } + }, + "type": "object", + "title": "JSONFunctionInputSchema" + }, + "JSONFunctionOutputSchema": { + "properties": { + "schema_content": { + "type": "object", + "title": "JSON Schema", + "description": "JSON Schema", + "default": {} + }, + "schema_class": { + "type": "string", + "const": "application/schema+json", + "title": "Schema Class", + "default": "application/schema+json" + } + }, + "type": "object", + "title": "JSONFunctionOutputSchema" + }, + "Job": { + "properties": { + "id": { + "type": "string", + "format": "uuid", + "title": "Id" + }, + "name": { + "type": "string", + "pattern": "^([^\\s/]+/?){1,10}$", + "title": "Name" + }, + "inputs_checksum": { + "type": "string", + "title": "Inputs Checksum", + "description": "Input's checksum" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At", + "description": "Job creation timestamp" + }, + "runner_name": { + "type": "string", + "pattern": "^([^\\s/]+/?){1,10}$", + "title": "Runner Name", + "description": "Runner that executes job" + }, + "url": { + "anyOf": [ + { + "type": "string", + "maxLength": 2083, + "minLength": 1, + "format": "uri" + }, + { + "type": "null" + } + ], + "title": "Url", + "description": "Link to get this resource (self)" + }, + "runner_url": { + "anyOf": [ + { + "type": "string", + "maxLength": 2083, + "minLength": 1, + "format": "uri" + }, + { + "type": "null" + } + ], + "title": "Runner Url", + "description": "Link to the solver's job (parent collection)" + }, + "outputs_url": { + "anyOf": [ + { + "type": "string", + "maxLength": 2083, + "minLength": 1, + "format": "uri" + }, + { + "type": "null" + } + ], + "title": "Outputs Url", + "description": "Link to the job outputs (sub-collection)" + } + }, + "type": "object", + "required": [ + "id", + "name", + "inputs_checksum", + "created_at", + "runner_name", + "url", + "runner_url", + "outputs_url" + ], + "title": "Job", + "example": { + "created_at": "2021-01-22T23:59:52.322176", + "id": "f622946d-fd29-35b9-a193-abdd1095167c", + "inputs_checksum": "12345", + "name": "solvers/isolve/releases/1.3.4/jobs/f622946d-fd29-35b9-a193-abdd1095167c", + "outputs_url": "https://api.osparc.io/v0/solvers/isolve/releases/1.3.4/jobs/f622946d-fd29-35b9-a193-abdd1095167c/outputs", + "runner_name": "solvers/isolve/releases/1.3.4", + "runner_url": "https://api.osparc.io/v0/solvers/isolve/releases/1.3.4", + "url": "https://api.osparc.io/v0/solvers/isolve/releases/1.3.4/jobs/f622946d-fd29-35b9-a193-abdd1095167c" + } + }, + "JobInputs": { + "properties": { + "values": { + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/components/schemas/File" + }, + { + "type": "number" + }, + { + "type": "integer" + }, + { + "type": "boolean" + }, + { + "type": "string" + }, + { + "items": {}, + "type": "array" + }, + { + "type": "null" + } + ] + }, + "type": "object", + "title": "Values" + } + }, + "type": "object", + "required": [ + "values" + ], + "title": "JobInputs", + "example": { + "values": { + "enabled": true, + "input_file": { + "filename": "input.txt", + "id": "0a3b2c56-dbcd-4871-b93b-d454b7883f9f" + }, + "n": 55, + "title": "Temperature", + "x": 4.33 + } + } + }, + "JobLog": { + "properties": { + "job_id": { + "type": "string", + "format": "uuid", + "title": "Job Id" + }, + "node_id": { + "anyOf": [ + { + "type": "string", + "format": "uuid" + }, + { + "type": "null" + } + ], + "title": "Node Id" + }, + "log_level": { + "type": "integer", + "title": "Log Level" + }, + "messages": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Messages" + } + }, + "type": "object", + "required": [ + "job_id", + "log_level", + "messages" + ], + "title": "JobLog", + "example": { + "job_id": "145beae4-a3a8-4fde-adbb-4e8257c2c083", + "log_level": 10, + "messages": [ + "PROGRESS: 5/10" + ], + "node_id": "3742215e-6756-48d2-8b73-4d043065309f" + } + }, + "JobLogsMap": { + "properties": { + "log_links": { + "items": { + "$ref": "#/components/schemas/LogLink" + }, + "type": "array", + "title": "Log Links", + "description": "Array of download links" + } + }, + "type": "object", + "required": [ + "log_links" + ], + "title": "JobLogsMap" + }, + "JobMetadata": { + "properties": { + "job_id": { + "type": "string", + "format": "uuid", + "title": "Job Id", + "description": "Parent Job" + }, + "metadata": { + "additionalProperties": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "string" + } + ] + }, + "type": "object", + "title": "Metadata", + "description": "Custom key-value map" + }, + "url": { + "anyOf": [ + { + "type": "string", + "maxLength": 2083, + "minLength": 1, + "format": "uri" + }, + { + "type": "null" + } + ], + "title": "Url", + "description": "Link to get this resource (self)" + } + }, + "type": "object", + "required": [ + "job_id", + "metadata", + "url" + ], + "title": "JobMetadata", + "example": { + "job_id": "3497e4de-0e69-41fb-b08f-7f3875a1ac4b", + "metadata": { + "bool": "true", + "float": "3.14", + "int": "42", + "str": "hej med dig" + }, + "url": "https://f02b2452-1dd8-4882-b673-af06373b41b3.fake" + } + }, + "JobMetadataUpdate": { + "properties": { + "metadata": { + "additionalProperties": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "integer" + }, + { + "type": "number" + }, + { + "type": "string" + } + ] + }, + "type": "object", + "title": "Metadata", + "description": "Custom key-value map" + } + }, + "type": "object", + "title": "JobMetadataUpdate", + "example": { + "metadata": { + "bool": "true", + "float": "3.14", + "int": "42", + "str": "hej med dig" + } + } + }, + "JobOutputs": { + "properties": { + "job_id": { + "type": "string", + "format": "uuid", + "title": "Job Id", + "description": "Job that produced this output" + }, + "results": { + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/components/schemas/File" + }, + { + "type": "number" + }, + { + "type": "integer" + }, + { + "type": "boolean" + }, + { + "type": "string" + }, + { + "items": {}, + "type": "array" + }, + { + "type": "null" + } + ] + }, + "type": "object", + "title": "Results" + } + }, + "type": "object", + "required": [ + "job_id", + "results" + ], + "title": "JobOutputs", + "example": { + "job_id": "99d9ac65-9f10-4e2f-a433-b5e412bb037b", + "results": { + "enabled": false, + "maxSAR": 4.33, + "n": 55, + "output_file": { + "filename": "sar_matrix.txt", + "id": "0a3b2c56-dbcd-4871-b93b-d454b7883f9f" + }, + "title": "Specific Absorption Rate" + } + } + }, + "JobStatus": { + "properties": { + "job_id": { + "type": "string", + "format": "uuid", + "title": "Job Id" + }, + "state": { + "$ref": "#/components/schemas/RunningState" + }, + "progress": { + "type": "integer", + "maximum": 100, + "minimum": 0, + "title": "Progress", + "default": 0 + }, + "submitted_at": { + "type": "string", + "format": "date-time", + "title": "Submitted At", + "description": "Last modification timestamp of the solver job" + }, + "started_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Started At", + "description": "Timestamp that indicate the moment the solver starts execution or None if the event did not occur" + }, + "stopped_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Stopped At", + "description": "Timestamp at which the solver finished or killed execution or None if the event did not occur" + } + }, + "type": "object", + "required": [ + "job_id", + "state", + "submitted_at" + ], + "title": "JobStatus", + "example": { + "job_id": "145beae4-a3a8-4fde-adbb-4e8257c2c083", + "progress": 3, + "started_at": "2021-04-01 07:16:43.670610", + "state": "STARTED", + "submitted_at": "2021-04-01 07:15:54.631007" + } + }, + "LicensedItemCheckoutData": { + "properties": { + "number_of_seats": { + "type": "integer", + "exclusiveMinimum": true, + "title": "Number Of Seats", + "minimum": 0 + }, + "service_run_id": { + "type": "string", + "title": "Service Run Id" + } + }, + "type": "object", + "required": [ + "number_of_seats", + "service_run_id" + ], + "title": "LicensedItemCheckoutData" + }, + "LicensedItemCheckoutGet": { + "properties": { + "licensed_item_checkout_id": { + "type": "string", + "format": "uuid", + "title": "Licensed Item Checkout Id" + }, + "licensed_item_id": { + "type": "string", + "format": "uuid", + "title": "Licensed Item Id" + }, + "key": { + "type": "string", + "title": "Key" + }, + "version": { + "type": "string", + "pattern": "^\\d+\\.\\d+\\.\\d+$", + "title": "Version" + }, + "wallet_id": { + "type": "integer", + "exclusiveMinimum": true, + "title": "Wallet Id", + "minimum": 0 + }, + "user_id": { + "type": "integer", + "exclusiveMinimum": true, + "title": "User Id", + "minimum": 0 + }, + "product_name": { + "type": "string", + "title": "Product Name" + }, + "started_at": { + "type": "string", + "format": "date-time", + "title": "Started At" + }, + "stopped_at": { + "anyOf": [ + { + "type": "string", + "format": "date-time" + }, + { + "type": "null" + } + ], + "title": "Stopped At" + }, + "num_of_seats": { + "type": "integer", + "title": "Num Of Seats" + } + }, + "type": "object", + "required": [ + "licensed_item_checkout_id", + "licensed_item_id", + "key", + "version", + "wallet_id", + "user_id", + "product_name", + "started_at", + "stopped_at", + "num_of_seats" + ], + "title": "LicensedItemCheckoutGet" + }, + "LicensedItemGet": { + "properties": { + "licensed_item_id": { + "type": "string", + "format": "uuid", + "title": "Licensed Item Id" + }, + "key": { + "type": "string", + "title": "Key" + }, + "version": { + "type": "string", + "pattern": "^\\d+\\.\\d+\\.\\d+$", + "title": "Version" + }, + "display_name": { + "type": "string", + "title": "Display Name" + }, + "licensed_resource_type": { + "$ref": "#/components/schemas/LicensedResourceType" + }, + "licensed_resources": { + "items": { + "$ref": "#/components/schemas/LicensedResource" + }, + "type": "array", + "title": "Licensed Resources" + }, + "pricing_plan_id": { + "type": "integer", + "exclusiveMinimum": true, + "title": "Pricing Plan Id", + "minimum": 0 + }, + "is_hidden_on_market": { + "type": "boolean", + "title": "Is Hidden On Market" + }, + "created_at": { + "type": "string", + "format": "date-time", + "title": "Created At" + }, + "modified_at": { + "type": "string", + "format": "date-time", + "title": "Modified At" + } + }, + "type": "object", + "required": [ + "licensed_item_id", + "key", + "version", + "display_name", + "licensed_resource_type", + "licensed_resources", + "pricing_plan_id", + "is_hidden_on_market", + "created_at", + "modified_at" + ], + "title": "LicensedItemGet" + }, + "LicensedResource": { + "properties": { + "source": { + "$ref": "#/components/schemas/LicensedResourceSource" + }, + "category_id": { + "type": "string", + "maxLength": 100, + "minLength": 1, + "title": "Category Id" + }, + "category_display": { + "type": "string", + "title": "Category Display" + }, + "terms_of_use_url": { "anyOf": [ { - "$ref": "#/components/schemas/UserFileToProgramJob" + "type": "string", + "maxLength": 2083, + "minLength": 1, + "format": "uri" }, { - "$ref": "#/components/schemas/UserFile" + "type": "null" } ], - "title": "Client File" + "title": "Terms Of Use Url" + } + }, + "type": "object", + "required": [ + "source", + "category_id", + "category_display", + "terms_of_use_url" + ], + "title": "LicensedResource" + }, + "LicensedResourceSource": { + "properties": { + "id": { + "type": "integer", + "title": "Id" + }, + "description": { + "type": "string", + "title": "Description" + }, + "thumbnail": { + "type": "string", + "title": "Thumbnail" + }, + "features": { + "$ref": "#/components/schemas/LicensedResourceSourceFeaturesDict" + }, + "doi": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Doi" + }, + "license_key": { + "type": "string", + "title": "License Key" + }, + "license_version": { + "type": "string", + "title": "License Version" + }, + "protection": { + "type": "string", + "enum": [ + "Code", + "PayPal" + ], + "title": "Protection" + }, + "available_from_url": { + "anyOf": [ + { + "type": "string", + "maxLength": 2083, + "minLength": 1, + "format": "uri" + }, + { + "type": "null" + } + ], + "title": "Available From Url" + } + }, + "type": "object", + "required": [ + "id", + "description", + "thumbnail", + "features", + "doi", + "license_key", + "license_version", + "protection", + "available_from_url" + ], + "title": "LicensedResourceSource" + }, + "LicensedResourceSourceFeaturesDict": { + "properties": { + "age": { + "type": "string", + "title": "Age" + }, + "date": { + "type": "string", + "format": "date", + "title": "Date" + }, + "ethnicity": { + "type": "string", + "title": "Ethnicity" + }, + "functionality": { + "type": "string", + "title": "Functionality" + }, + "height": { + "type": "string", + "title": "Height" + }, + "name": { + "type": "string", + "title": "Name" + }, + "sex": { + "type": "string", + "title": "Sex" + }, + "species": { + "type": "string", + "title": "Species" + }, + "version": { + "type": "string", + "title": "Version" + }, + "weight": { + "type": "string", + "title": "Weight" } }, "type": "object", "required": [ - "client_file" + "date" ], - "title": "Body_abort_multipart_upload_v0_files__file_id__abort_post" + "title": "LicensedResourceSourceFeaturesDict" }, - "Body_complete_multipart_upload_v0_files__file_id__complete_post": { - "properties": { - "client_file": { - "anyOf": [ - { - "$ref": "#/components/schemas/UserFileToProgramJob" - }, - { - "$ref": "#/components/schemas/UserFile" - } - ], - "title": "Client File" - }, - "uploaded_parts": { - "$ref": "#/components/schemas/FileUploadCompletionBody" - } - }, - "type": "object", - "required": [ - "client_file", - "uploaded_parts" + "LicensedResourceType": { + "type": "string", + "enum": [ + "VIP_MODEL" ], - "title": "Body_complete_multipart_upload_v0_files__file_id__complete_post" + "title": "LicensedResourceType" }, - "Body_create_program_job_v0_programs__program_key__releases__version__jobs_post": { + "Links": { "properties": { - "name": { + "first": { "anyOf": [ { - "type": "string", - "maxLength": 500 + "type": "string" }, { "type": "null" } ], - "title": "Name" + "title": "First" }, - "description": { + "last": { "anyOf": [ { - "type": "string", - "maxLength": 500 + "type": "string" }, { "type": "null" } ], - "title": "Description" - } - }, - "type": "object", - "title": "Body_create_program_job_v0_programs__program_key__releases__version__jobs_post" - }, - "Body_upload_file_v0_files_content_put": { - "properties": { - "file": { - "type": "string", - "format": "binary", - "title": "File" - } - }, - "type": "object", - "required": [ - "file" - ], - "title": "Body_upload_file_v0_files_content_put" - }, - "ClientFileUploadData": { - "properties": { - "file_id": { - "type": "string", - "format": "uuid", - "title": "File Id", - "description": "The file resource id" - }, - "upload_schema": { - "$ref": "#/components/schemas/FileUploadData", - "description": "Schema for uploading file" - } - }, - "type": "object", - "required": [ - "file_id", - "upload_schema" - ], - "title": "ClientFileUploadData" - }, - "ErrorGet": { - "properties": { - "errors": { - "items": {}, - "type": "array", - "title": "Errors" - } - }, - "type": "object", - "required": [ - "errors" - ], - "title": "ErrorGet", - "example": { - "errors": [ - "some error message", - "another error message" - ] - } - }, - "File": { - "properties": { - "id": { - "type": "string", - "format": "uuid", - "title": "Id", - "description": "Resource identifier" - }, - "filename": { - "type": "string", - "title": "Filename", - "description": "Name of the file with extension" + "title": "Last" }, - "content_type": { + "self": { "anyOf": [ { "type": "string" @@ -6171,23 +8598,20 @@ "type": "null" } ], - "title": "Content Type", - "description": "Guess of type content [EXPERIMENTAL]" + "title": "Self" }, - "checksum": { + "next": { "anyOf": [ { - "type": "string", - "pattern": "^[a-fA-F0-9]{64}$" + "type": "string" }, { "type": "null" } ], - "title": "Checksum", - "description": "SHA256 hash of the file's content" + "title": "Next" }, - "e_tag": { + "prev": { "anyOf": [ { "type": "string" @@ -6196,82 +8620,110 @@ "type": "null" } ], - "title": "E Tag", - "description": "S3 entity tag" - } - }, - "type": "object", - "required": [ - "id", - "filename" - ], - "title": "File", - "description": "Represents a file stored on the server side i.e. a unique reference to a file in the cloud." - }, - "FileUploadCompletionBody": { - "properties": { - "parts": { - "items": { - "$ref": "#/components/schemas/UploadedPart" - }, - "type": "array", - "title": "Parts" + "title": "Prev" } }, "type": "object", "required": [ - "parts" + "first", + "last", + "self", + "next", + "prev" ], - "title": "FileUploadCompletionBody" + "title": "Links" }, - "FileUploadData": { + "LogLink": { "properties": { - "chunk_size": { - "type": "integer", - "minimum": 0, - "title": "Chunk Size" - }, - "urls": { - "items": { - "type": "string", - "minLength": 1, - "format": "uri" - }, - "type": "array", - "title": "Urls" + "node_name": { + "type": "string", + "title": "Node Name" }, - "links": { - "$ref": "#/components/schemas/UploadLinks" + "download_link": { + "type": "string", + "minLength": 1, + "format": "uri", + "title": "Download Link" } }, "type": "object", "required": [ - "chunk_size", - "urls", - "links" + "node_name", + "download_link" ], - "title": "FileUploadData" + "title": "LogLink" }, - "GetCreditPriceLegacy": { + "Meta": { "properties": { - "productName": { + "name": { "type": "string", - "title": "Productname" + "title": "Name" }, - "usdPerCredit": { + "version": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$", + "title": "Version" + }, + "released": { "anyOf": [ { - "type": "number", - "minimum": 0.0 + "additionalProperties": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$" + }, + "type": "object" }, { "type": "null" } ], - "title": "Usdpercredit", - "description": "Price of a credit in USD. If None, then this product's price is UNDEFINED" + "title": "Released", + "description": "Maps every route's path tag with a released version" + }, + "docs_url": { + "type": "string", + "maxLength": 2083, + "minLength": 1, + "format": "uri", + "title": "Docs Url" + }, + "docs_dev_url": { + "type": "string", + "maxLength": 2083, + "minLength": 1, + "format": "uri", + "title": "Docs Dev Url" + } + }, + "type": "object", + "required": [ + "name", + "version", + "docs_url", + "docs_dev_url" + ], + "title": "Meta", + "example": { + "docs_dev_url": "https://api.osparc.io/dev/doc", + "docs_url": "https://api.osparc.io/dev/doc", + "name": "simcore_service_foo", + "released": { + "v1": "1.3.4", + "v2": "2.4.45" }, - "minPaymentAmountUsd": { + "version": "2.4.45" + } + }, + "OnePage_SolverPort_": { + "properties": { + "items": { + "items": { + "$ref": "#/components/schemas/SolverPort" + }, + "type": "array", + "title": "Items" + }, + "total": { "anyOf": [ { "type": "integer", @@ -6281,873 +8733,706 @@ "type": "null" } ], - "title": "Minpaymentamountusd", - "description": "Minimum amount (included) in USD that can be paid for this productCan be None if this product's price is UNDEFINED" + "title": "Total" } }, "type": "object", "required": [ - "productName", - "usdPerCredit", - "minPaymentAmountUsd" + "items" ], - "title": "GetCreditPriceLegacy" + "title": "OnePage[SolverPort]" }, - "Groups": { + "OnePage_StudyPort_": { "properties": { - "me": { - "$ref": "#/components/schemas/UsersGroup" + "items": { + "items": { + "$ref": "#/components/schemas/StudyPort" + }, + "type": "array", + "title": "Items" }, - "organizations": { + "total": { "anyOf": [ { - "items": { - "$ref": "#/components/schemas/UsersGroup" - }, - "type": "array" + "type": "integer", + "minimum": 0 }, { "type": "null" } ], - "title": "Organizations", - "default": [] - }, - "all": { - "$ref": "#/components/schemas/UsersGroup" + "title": "Total" } }, "type": "object", "required": [ - "me", - "all" + "items" ], - "title": "Groups" + "title": "OnePage[StudyPort]" }, - "HTTPValidationError": { + "Page_Annotated_Union_RegisteredProjectFunctionJob__RegisteredPythonCodeFunctionJob__RegisteredSolverFunctionJob___FieldInfo_annotation_NoneType__required_True__discriminator__function_class____": { "properties": { - "errors": { + "items": { "items": { - "$ref": "#/components/schemas/ValidationError" + "oneOf": [ + { + "$ref": "#/components/schemas/RegisteredProjectFunctionJob" + }, + { + "$ref": "#/components/schemas/RegisteredPythonCodeFunctionJob" + }, + { + "$ref": "#/components/schemas/RegisteredSolverFunctionJob" + } + ], + "discriminator": { + "propertyName": "function_class", + "mapping": { + "project": "#/components/schemas/RegisteredProjectFunctionJob", + "python_code": "#/components/schemas/RegisteredPythonCodeFunctionJob", + "solver": "#/components/schemas/RegisteredSolverFunctionJob" + } + } }, "type": "array", - "title": "Validation errors" - } - }, - "type": "object", - "title": "HTTPValidationError" - }, - "Job": { - "properties": { - "id": { - "type": "string", - "format": "uuid", - "title": "Id" - }, - "name": { - "type": "string", - "pattern": "^([^\\s/]+/?){1,10}$", - "title": "Name" - }, - "inputs_checksum": { - "type": "string", - "title": "Inputs Checksum", - "description": "Input's checksum" - }, - "created_at": { - "type": "string", - "format": "date-time", - "title": "Created At", - "description": "Job creation timestamp" - }, - "runner_name": { - "type": "string", - "pattern": "^([^\\s/]+/?){1,10}$", - "title": "Runner Name", - "description": "Runner that executes job" + "title": "Items" }, - "url": { + "total": { "anyOf": [ { - "type": "string", - "maxLength": 2083, - "minLength": 1, - "format": "uri" + "type": "integer", + "minimum": 0 }, { "type": "null" } ], - "title": "Url", - "description": "Link to get this resource (self)" + "title": "Total" }, - "runner_url": { + "limit": { "anyOf": [ { - "type": "string", - "maxLength": 2083, - "minLength": 1, - "format": "uri" + "type": "integer", + "minimum": 1 }, { "type": "null" } ], - "title": "Runner Url", - "description": "Link to the solver's job (parent collection)" + "title": "Limit" }, - "outputs_url": { + "offset": { "anyOf": [ { - "type": "string", - "maxLength": 2083, - "minLength": 1, - "format": "uri" + "type": "integer", + "minimum": 0 }, { "type": "null" } ], - "title": "Outputs Url", - "description": "Link to the job outputs (sub-collection)" + "title": "Offset" + }, + "links": { + "$ref": "#/components/schemas/Links" } }, "type": "object", "required": [ - "id", - "name", - "inputs_checksum", - "created_at", - "runner_name", - "url", - "runner_url", - "outputs_url" + "items", + "total", + "limit", + "offset", + "links" ], - "title": "Job", - "example": { - "created_at": "2021-01-22T23:59:52.322176", - "id": "f622946d-fd29-35b9-a193-abdd1095167c", - "inputs_checksum": "12345", - "name": "solvers/isolve/releases/1.3.4/jobs/f622946d-fd29-35b9-a193-abdd1095167c", - "outputs_url": "https://api.osparc.io/v0/solvers/isolve/releases/1.3.4/jobs/f622946d-fd29-35b9-a193-abdd1095167c/outputs", - "runner_name": "solvers/isolve/releases/1.3.4", - "runner_url": "https://api.osparc.io/v0/solvers/isolve/releases/1.3.4", - "url": "https://api.osparc.io/v0/solvers/isolve/releases/1.3.4/jobs/f622946d-fd29-35b9-a193-abdd1095167c" - } + "title": "Page[Annotated[Union[RegisteredProjectFunctionJob, RegisteredPythonCodeFunctionJob, RegisteredSolverFunctionJob], FieldInfo(annotation=NoneType, required=True, discriminator='function_class')]]" }, - "JobInputs": { + "Page_Annotated_Union_RegisteredProjectFunction__RegisteredPythonCodeFunction__RegisteredSolverFunction___FieldInfo_annotation_NoneType__required_True__discriminator__function_class____": { "properties": { - "values": { - "additionalProperties": { - "anyOf": [ - { - "$ref": "#/components/schemas/File" - }, - { - "type": "number" - }, - { - "type": "integer" - }, - { - "type": "boolean" - }, + "items": { + "items": { + "oneOf": [ { - "type": "string" + "$ref": "#/components/schemas/RegisteredProjectFunction" }, { - "items": {}, - "type": "array" + "$ref": "#/components/schemas/RegisteredPythonCodeFunction" }, { - "type": "null" + "$ref": "#/components/schemas/RegisteredSolverFunction" } - ] + ], + "discriminator": { + "propertyName": "function_class", + "mapping": { + "project": "#/components/schemas/RegisteredProjectFunction", + "python_code": "#/components/schemas/RegisteredPythonCodeFunction", + "solver": "#/components/schemas/RegisteredSolverFunction" + } + } }, - "type": "object", - "title": "Values" + "type": "array", + "title": "Items" + }, + "total": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ], + "title": "Total" + }, + "limit": { + "anyOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "null" + } + ], + "title": "Limit" + }, + "offset": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ], + "title": "Offset" + }, + "links": { + "$ref": "#/components/schemas/Links" } }, "type": "object", "required": [ - "values" + "items", + "total", + "limit", + "offset", + "links" ], - "title": "JobInputs", - "example": { - "values": { - "enabled": true, - "input_file": { - "filename": "input.txt", - "id": "0a3b2c56-dbcd-4871-b93b-d454b7883f9f" - }, - "n": 55, - "title": "Temperature", - "x": 4.33 - } - } + "title": "Page[Annotated[Union[RegisteredProjectFunction, RegisteredPythonCodeFunction, RegisteredSolverFunction], FieldInfo(annotation=NoneType, required=True, discriminator='function_class')]]" }, - "JobLog": { + "Page_File_": { "properties": { - "job_id": { - "type": "string", - "format": "uuid", - "title": "Job Id" + "items": { + "items": { + "$ref": "#/components/schemas/File" + }, + "type": "array", + "title": "Items" }, - "node_id": { + "total": { "anyOf": [ { - "type": "string", - "format": "uuid" + "type": "integer", + "minimum": 0 }, { "type": "null" } ], - "title": "Node Id" + "title": "Total" }, - "log_level": { - "type": "integer", - "title": "Log Level" + "limit": { + "anyOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "null" + } + ], + "title": "Limit" }, - "messages": { - "items": { - "type": "string" - }, - "type": "array", - "title": "Messages" + "offset": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ], + "title": "Offset" + }, + "links": { + "$ref": "#/components/schemas/Links" } }, "type": "object", "required": [ - "job_id", - "log_level", - "messages" + "items", + "total", + "limit", + "offset", + "links" ], - "title": "JobLog", - "example": { - "job_id": "145beae4-a3a8-4fde-adbb-4e8257c2c083", - "log_level": 10, - "messages": [ - "PROGRESS: 5/10" - ], - "node_id": "3742215e-6756-48d2-8b73-4d043065309f" - } + "title": "Page[File]" }, - "JobLogsMap": { + "Page_Job_": { "properties": { - "log_links": { + "items": { "items": { - "$ref": "#/components/schemas/LogLink" + "$ref": "#/components/schemas/Job" }, "type": "array", - "title": "Log Links", - "description": "Array of download links" - } - }, - "type": "object", - "required": [ - "log_links" - ], - "title": "JobLogsMap" - }, - "JobMetadata": { - "properties": { - "job_id": { - "type": "string", - "format": "uuid", - "title": "Job Id", - "description": "Parent Job" + "title": "Items" }, - "metadata": { - "additionalProperties": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "integer" - }, - { - "type": "number" - }, - { - "type": "string" - } - ] - }, - "type": "object", - "title": "Metadata", - "description": "Custom key-value map" + "total": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ], + "title": "Total" }, - "url": { + "limit": { "anyOf": [ { - "type": "string", - "maxLength": 2083, - "minLength": 1, - "format": "uri" + "type": "integer", + "minimum": 1 }, { "type": "null" } ], - "title": "Url", - "description": "Link to get this resource (self)" - } - }, - "type": "object", - "required": [ - "job_id", - "metadata", - "url" - ], - "title": "JobMetadata", - "example": { - "job_id": "3497e4de-0e69-41fb-b08f-7f3875a1ac4b", - "metadata": { - "bool": "true", - "float": "3.14", - "int": "42", - "str": "hej med dig" + "title": "Limit" }, - "url": "https://f02b2452-1dd8-4882-b673-af06373b41b3.fake" - } - }, - "JobMetadataUpdate": { - "properties": { - "metadata": { - "additionalProperties": { - "anyOf": [ - { - "type": "boolean" - }, - { - "type": "integer" - }, - { - "type": "number" - }, - { - "type": "string" - } - ] - }, - "type": "object", - "title": "Metadata", - "description": "Custom key-value map" - } - }, - "type": "object", - "title": "JobMetadataUpdate", - "example": { - "metadata": { - "bool": "true", - "float": "3.14", - "int": "42", - "str": "hej med dig" - } - } - }, - "JobOutputs": { - "properties": { - "job_id": { - "type": "string", - "format": "uuid", - "title": "Job Id", - "description": "Job that produced this output" + "offset": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ], + "title": "Offset" }, - "results": { - "additionalProperties": { - "anyOf": [ - { - "$ref": "#/components/schemas/File" - }, - { - "type": "number" - }, - { - "type": "integer" - }, - { - "type": "boolean" - }, - { - "type": "string" - }, - { - "items": {}, - "type": "array" - }, - { - "type": "null" - } - ] - }, - "type": "object", - "title": "Results" + "links": { + "$ref": "#/components/schemas/Links" } }, "type": "object", "required": [ - "job_id", - "results" + "items", + "total", + "limit", + "offset", + "links" ], - "title": "JobOutputs", - "example": { - "job_id": "99d9ac65-9f10-4e2f-a433-b5e412bb037b", - "results": { - "enabled": false, - "maxSAR": 4.33, - "n": 55, - "output_file": { - "filename": "sar_matrix.txt", - "id": "0a3b2c56-dbcd-4871-b93b-d454b7883f9f" - }, - "title": "Specific Absorption Rate" - } - } + "title": "Page[Job]" }, - "JobStatus": { + "Page_LicensedItemGet_": { "properties": { - "job_id": { - "type": "string", - "format": "uuid", - "title": "Job Id" - }, - "state": { - "$ref": "#/components/schemas/RunningState" - }, - "progress": { - "type": "integer", - "maximum": 100, - "minimum": 0, - "title": "Progress", - "default": 0 + "items": { + "items": { + "$ref": "#/components/schemas/LicensedItemGet" + }, + "type": "array", + "title": "Items" }, - "submitted_at": { - "type": "string", - "format": "date-time", - "title": "Submitted At", - "description": "Last modification timestamp of the solver job" + "total": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ], + "title": "Total" }, - "started_at": { + "limit": { "anyOf": [ { - "type": "string", - "format": "date-time" + "type": "integer", + "minimum": 1 }, { "type": "null" } ], - "title": "Started At", - "description": "Timestamp that indicate the moment the solver starts execution or None if the event did not occur" + "title": "Limit" }, - "stopped_at": { + "offset": { "anyOf": [ { - "type": "string", - "format": "date-time" + "type": "integer", + "minimum": 0 }, { "type": "null" } ], - "title": "Stopped At", - "description": "Timestamp at which the solver finished or killed execution or None if the event did not occur" - } - }, - "type": "object", - "required": [ - "job_id", - "state", - "submitted_at" - ], - "title": "JobStatus", - "example": { - "job_id": "145beae4-a3a8-4fde-adbb-4e8257c2c083", - "progress": 3, - "started_at": "2021-04-01 07:16:43.670610", - "state": "STARTED", - "submitted_at": "2021-04-01 07:15:54.631007" - } - }, - "LicensedItemCheckoutData": { - "properties": { - "number_of_seats": { - "type": "integer", - "exclusiveMinimum": true, - "title": "Number Of Seats", - "minimum": 0 + "title": "Offset" }, - "service_run_id": { - "type": "string", - "title": "Service Run Id" + "links": { + "$ref": "#/components/schemas/Links" } }, "type": "object", "required": [ - "number_of_seats", - "service_run_id" + "items", + "total", + "limit", + "offset", + "links" ], - "title": "LicensedItemCheckoutData" + "title": "Page[LicensedItemGet]" }, - "LicensedItemCheckoutGet": { + "Page_RegisteredFunctionJobCollection_": { "properties": { - "licensed_item_checkout_id": { - "type": "string", - "format": "uuid", - "title": "Licensed Item Checkout Id" - }, - "licensed_item_id": { - "type": "string", - "format": "uuid", - "title": "Licensed Item Id" - }, - "key": { - "type": "string", - "title": "Key" - }, - "version": { - "type": "string", - "pattern": "^\\d+\\.\\d+\\.\\d+$", - "title": "Version" - }, - "wallet_id": { - "type": "integer", - "exclusiveMinimum": true, - "title": "Wallet Id", - "minimum": 0 - }, - "user_id": { - "type": "integer", - "exclusiveMinimum": true, - "title": "User Id", - "minimum": 0 + "items": { + "items": { + "$ref": "#/components/schemas/RegisteredFunctionJobCollection" + }, + "type": "array", + "title": "Items" }, - "product_name": { - "type": "string", - "title": "Product Name" + "total": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ], + "title": "Total" }, - "started_at": { - "type": "string", - "format": "date-time", - "title": "Started At" + "limit": { + "anyOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "null" + } + ], + "title": "Limit" }, - "stopped_at": { + "offset": { "anyOf": [ { - "type": "string", - "format": "date-time" + "type": "integer", + "minimum": 0 }, { "type": "null" } ], - "title": "Stopped At" + "title": "Offset" }, - "num_of_seats": { - "type": "integer", - "title": "Num Of Seats" + "links": { + "$ref": "#/components/schemas/Links" } }, "type": "object", "required": [ - "licensed_item_checkout_id", - "licensed_item_id", - "key", - "version", - "wallet_id", - "user_id", - "product_name", - "started_at", - "stopped_at", - "num_of_seats" + "items", + "total", + "limit", + "offset", + "links" ], - "title": "LicensedItemCheckoutGet" + "title": "Page[RegisteredFunctionJobCollection]" }, - "LicensedItemGet": { + "Page_Study_": { "properties": { - "licensed_item_id": { - "type": "string", - "format": "uuid", - "title": "Licensed Item Id" - }, - "key": { - "type": "string", - "title": "Key" - }, - "version": { - "type": "string", - "pattern": "^\\d+\\.\\d+\\.\\d+$", - "title": "Version" - }, - "display_name": { - "type": "string", - "title": "Display Name" - }, - "licensed_resource_type": { - "$ref": "#/components/schemas/LicensedResourceType" - }, - "licensed_resources": { + "items": { "items": { - "$ref": "#/components/schemas/LicensedResource" + "$ref": "#/components/schemas/Study" }, "type": "array", - "title": "Licensed Resources" + "title": "Items" }, - "pricing_plan_id": { + "total": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ], + "title": "Total" + }, + "limit": { + "anyOf": [ + { + "type": "integer", + "minimum": 1 + }, + { + "type": "null" + } + ], + "title": "Limit" + }, + "offset": { + "anyOf": [ + { + "type": "integer", + "minimum": 0 + }, + { + "type": "null" + } + ], + "title": "Offset" + }, + "links": { + "$ref": "#/components/schemas/Links" + } + }, + "type": "object", + "required": [ + "items", + "total", + "limit", + "offset", + "links" + ], + "title": "Page[Study]" + }, + "PricingPlanClassification": { + "type": "string", + "enum": [ + "TIER", + "LICENSE" + ], + "title": "PricingPlanClassification" + }, + "PricingUnitGetLegacy": { + "properties": { + "pricingUnitId": { "type": "integer", "exclusiveMinimum": true, - "title": "Pricing Plan Id", + "title": "Pricingunitid", "minimum": 0 }, - "is_hidden_on_market": { - "type": "boolean", - "title": "Is Hidden On Market" - }, - "created_at": { + "unitName": { "type": "string", - "format": "date-time", - "title": "Created At" + "title": "Unitname" }, - "modified_at": { - "type": "string", - "format": "date-time", - "title": "Modified At" + "unitExtraInfo": { + "$ref": "#/components/schemas/UnitExtraInfoTier" + }, + "currentCostPerUnit": { + "type": "number", + "minimum": 0.0, + "title": "Currentcostperunit" + }, + "default": { + "type": "boolean", + "title": "Default" } }, "type": "object", "required": [ - "licensed_item_id", - "key", - "version", - "display_name", - "licensed_resource_type", - "licensed_resources", - "pricing_plan_id", - "is_hidden_on_market", - "created_at", - "modified_at" + "pricingUnitId", + "unitName", + "unitExtraInfo", + "currentCostPerUnit", + "default" ], - "title": "LicensedItemGet" + "title": "PricingUnitGetLegacy" }, - "LicensedResource": { + "Profile": { "properties": { - "source": { - "$ref": "#/components/schemas/LicensedResourceSource" - }, - "category_id": { - "type": "string", - "maxLength": 100, - "minLength": 1, - "title": "Category Id" - }, - "category_display": { - "type": "string", - "title": "Category Display" + "first_name": { + "anyOf": [ + { + "type": "string", + "maxLength": 255 + }, + { + "type": "null" + } + ], + "title": "First Name" }, - "terms_of_use_url": { + "last_name": { "anyOf": [ { "type": "string", - "maxLength": 2083, - "minLength": 1, - "format": "uri" + "maxLength": 255 }, { "type": "null" } ], - "title": "Terms Of Use Url" - } - }, - "type": "object", - "required": [ - "source", - "category_id", - "category_display", - "terms_of_use_url" - ], - "title": "LicensedResource" - }, - "LicensedResourceSource": { - "properties": { + "title": "Last Name" + }, "id": { "type": "integer", - "title": "Id" - }, - "description": { - "type": "string", - "title": "Description" + "exclusiveMinimum": true, + "title": "Id", + "minimum": 0 }, - "thumbnail": { + "login": { "type": "string", - "title": "Thumbnail" + "format": "email", + "title": "Login" }, - "features": { - "$ref": "#/components/schemas/LicensedResourceSourceFeaturesDict" + "role": { + "$ref": "#/components/schemas/UserRoleEnum" }, - "doi": { + "groups": { "anyOf": [ { - "type": "string" + "$ref": "#/components/schemas/Groups" }, { "type": "null" } - ], - "title": "Doi" - }, - "license_key": { - "type": "string", - "title": "License Key" - }, - "license_version": { - "type": "string", - "title": "License Version" - }, - "protection": { - "type": "string", - "enum": [ - "Code", - "PayPal" - ], - "title": "Protection" + ] }, - "available_from_url": { + "gravatar_id": { "anyOf": [ { "type": "string", - "maxLength": 2083, - "minLength": 1, - "format": "uri" + "maxLength": 40 }, { "type": "null" } ], - "title": "Available From Url" + "title": "Gravatar Id", + "description": "md5 hash value of email to retrieve an avatar image from https://www.gravatar.com" } }, "type": "object", "required": [ "id", - "description", - "thumbnail", - "features", - "doi", - "license_key", - "license_version", - "protection", - "available_from_url" + "login", + "role" ], - "title": "LicensedResourceSource" - }, - "LicensedResourceSourceFeaturesDict": { - "properties": { - "age": { - "type": "string", - "title": "Age" - }, - "date": { - "type": "string", - "format": "date", - "title": "Date" - }, - "ethnicity": { - "type": "string", - "title": "Ethnicity" - }, - "functionality": { - "type": "string", - "title": "Functionality" - }, - "height": { - "type": "string", - "title": "Height" - }, - "name": { - "type": "string", - "title": "Name" - }, - "sex": { - "type": "string", - "title": "Sex" - }, - "species": { - "type": "string", - "title": "Species" - }, - "version": { - "type": "string", - "title": "Version" + "title": "Profile", + "example": { + "first_name": "James", + "gravatar_id": "9a8930a5b20d7048e37740bac5c1ca4f", + "groups": { + "all": { + "description": "all users", + "gid": "1", + "label": "Everyone" + }, + "me": { + "description": "primary group", + "gid": "123", + "label": "maxy" + }, + "organizations": [] }, - "weight": { - "type": "string", - "title": "Weight" - } - }, - "type": "object", - "required": [ - "date" - ], - "title": "LicensedResourceSourceFeaturesDict" - }, - "LicensedResourceType": { - "type": "string", - "enum": [ - "VIP_MODEL" - ], - "title": "LicensedResourceType" + "id": "20", + "last_name": "Maxwell", + "login": "james-maxwell@itis.swiss", + "role": "USER" + } }, - "Links": { + "ProfileUpdate": { "properties": { - "first": { + "first_name": { "anyOf": [ { - "type": "string" + "type": "string", + "maxLength": 255 }, { "type": "null" } ], - "title": "First" + "title": "First Name" }, - "last": { + "last_name": { "anyOf": [ { - "type": "string" + "type": "string", + "maxLength": 255 }, { "type": "null" } ], - "title": "Last" + "title": "Last Name" + } + }, + "type": "object", + "title": "ProfileUpdate" + }, + "Program": { + "properties": { + "id": { + "type": "string", + "title": "Id", + "description": "Resource identifier" }, - "self": { + "version": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$", + "title": "Version", + "description": "Semantic version number of the resource" + }, + "title": { + "type": "string", + "maxLength": 100, + "title": "Title", + "description": "Human readable name" + }, + "description": { "anyOf": [ { - "type": "string" + "type": "string", + "maxLength": 1000 }, { "type": "null" } ], - "title": "Self" + "title": "Description", + "description": "Description of the resource" }, - "next": { + "url": { "anyOf": [ { - "type": "string" + "type": "string", + "maxLength": 2083, + "minLength": 1, + "format": "uri" }, { "type": "null" } ], - "title": "Next" + "title": "Url", + "description": "Link to get this resource" }, - "prev": { + "version_display": { "anyOf": [ { "type": "string" @@ -7156,636 +9441,749 @@ "type": "null" } ], - "title": "Prev" + "title": "Version Display" } }, "type": "object", "required": [ - "first", - "last", - "self", - "next", - "prev" + "id", + "version", + "title", + "url", + "version_display" ], - "title": "Links" + "title": "Program", + "description": "A released program with a specific version", + "example": { + "description": "Simulation framework", + "id": "simcore/services/dynamic/sim4life", + "maintainer": "info@itis.swiss", + "title": "Sim4life", + "url": "https://api.osparc.io/v0/solvers/simcore%2Fservices%2Fdynamic%2Fsim4life/releases/8.0.0", + "version": "8.0.0", + "version_display": "8.0.0" + } }, - "LogLink": { + "ProjectFunction": { "properties": { - "node_name": { + "function_class": { "type": "string", - "title": "Node Name" + "const": "project", + "title": "Function Class", + "default": "project" }, - "download_link": { - "type": "string", - "minLength": 1, - "format": "uri", - "title": "Download Link" - } - }, - "type": "object", - "required": [ - "node_name", - "download_link" - ], - "title": "LogLink" - }, - "Meta": { - "properties": { - "name": { + "title": { "type": "string", - "title": "Name" + "title": "Title", + "default": "" }, - "version": { + "description": { "type": "string", - "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$", - "title": "Version" + "title": "Description", + "default": "" }, - "released": { - "anyOf": [ - { - "additionalProperties": { - "type": "string", - "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$" - }, - "type": "object" - }, + "input_schema": { + "oneOf": [ { - "type": "null" + "$ref": "#/components/schemas/JSONFunctionInputSchema" } ], - "title": "Released", - "description": "Maps every route's path tag with a released version" - }, - "docs_url": { - "type": "string", - "maxLength": 2083, - "minLength": 1, - "format": "uri", - "title": "Docs Url" - }, - "docs_dev_url": { - "type": "string", - "maxLength": 2083, - "minLength": 1, - "format": "uri", - "title": "Docs Dev Url" - } - }, - "type": "object", - "required": [ - "name", - "version", - "docs_url", - "docs_dev_url" - ], - "title": "Meta", - "example": { - "docs_dev_url": "https://api.osparc.io/dev/doc", - "docs_url": "https://api.osparc.io/dev/doc", - "name": "simcore_service_foo", - "released": { - "v1": "1.3.4", - "v2": "2.4.45" - }, - "version": "2.4.45" - } - }, - "OnePage_SolverPort_": { - "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/SolverPort" - }, - "type": "array", - "title": "Items" + "title": "Input Schema", + "discriminator": { + "propertyName": "schema_class", + "mapping": { + "application/schema+json": "#/components/schemas/JSONFunctionInputSchema" + } + } }, - "total": { - "anyOf": [ - { - "type": "integer", - "minimum": 0 - }, + "output_schema": { + "oneOf": [ { - "type": "null" + "$ref": "#/components/schemas/JSONFunctionOutputSchema" } ], - "title": "Total" - } - }, - "type": "object", - "required": [ - "items" - ], - "title": "OnePage[SolverPort]" - }, - "OnePage_StudyPort_": { - "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/StudyPort" - }, - "type": "array", - "title": "Items" + "title": "Output Schema", + "discriminator": { + "propertyName": "schema_class", + "mapping": { + "application/schema+json": "#/components/schemas/JSONFunctionOutputSchema" + } + } }, - "total": { + "default_inputs": { "anyOf": [ { - "type": "integer", - "minimum": 0 + "type": "object" }, { "type": "null" } ], - "title": "Total" + "title": "Default Inputs" + }, + "project_id": { + "type": "string", + "format": "uuid", + "title": "Project Id" } }, "type": "object", "required": [ - "items" + "input_schema", + "output_schema", + "default_inputs", + "project_id" ], - "title": "OnePage[StudyPort]" + "title": "ProjectFunction" }, - "Page_File_": { + "ProjectFunctionJob": { "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/File" - }, - "type": "array", - "title": "Items" + "title": { + "type": "string", + "title": "Title", + "default": "" }, - "total": { - "anyOf": [ - { - "type": "integer", - "minimum": 0 - }, - { - "type": "null" - } - ], - "title": "Total" + "description": { + "type": "string", + "title": "Description", + "default": "" }, - "limit": { + "function_uid": { + "type": "string", + "format": "uuid", + "title": "Function Uid" + }, + "inputs": { "anyOf": [ { - "type": "integer", - "minimum": 1 + "type": "object" }, { "type": "null" } ], - "title": "Limit" + "title": "Inputs" }, - "offset": { + "outputs": { "anyOf": [ { - "type": "integer", - "minimum": 0 + "type": "object" }, { "type": "null" } ], - "title": "Offset" + "title": "Outputs" }, - "links": { - "$ref": "#/components/schemas/Links" + "function_class": { + "type": "string", + "const": "project", + "title": "Function Class", + "default": "project" + }, + "project_job_id": { + "type": "string", + "format": "uuid", + "title": "Project Job Id" } }, "type": "object", "required": [ - "items", - "total", - "limit", - "offset", - "links" + "function_uid", + "inputs", + "outputs", + "project_job_id" ], - "title": "Page[File]" + "title": "ProjectFunctionJob" }, - "Page_Job_": { + "PythonCodeFunction": { "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/Job" - }, - "type": "array", - "title": "Items" + "function_class": { + "type": "string", + "const": "python_code", + "title": "Function Class", + "default": "python_code" + }, + "title": { + "type": "string", + "title": "Title", + "default": "" }, - "total": { - "anyOf": [ - { - "type": "integer", - "minimum": 0 - }, + "description": { + "type": "string", + "title": "Description", + "default": "" + }, + "input_schema": { + "oneOf": [ { - "type": "null" + "$ref": "#/components/schemas/JSONFunctionInputSchema" } ], - "title": "Total" + "title": "Input Schema", + "discriminator": { + "propertyName": "schema_class", + "mapping": { + "application/schema+json": "#/components/schemas/JSONFunctionInputSchema" + } + } }, - "limit": { - "anyOf": [ - { - "type": "integer", - "minimum": 1 - }, + "output_schema": { + "oneOf": [ { - "type": "null" + "$ref": "#/components/schemas/JSONFunctionOutputSchema" } ], - "title": "Limit" + "title": "Output Schema", + "discriminator": { + "propertyName": "schema_class", + "mapping": { + "application/schema+json": "#/components/schemas/JSONFunctionOutputSchema" + } + } }, - "offset": { + "default_inputs": { "anyOf": [ { - "type": "integer", - "minimum": 0 + "type": "object" }, { "type": "null" } ], - "title": "Offset" + "title": "Default Inputs" }, - "links": { - "$ref": "#/components/schemas/Links" + "code_url": { + "type": "string", + "title": "Code Url" } }, "type": "object", "required": [ - "items", - "total", - "limit", - "offset", - "links" + "input_schema", + "output_schema", + "default_inputs", + "code_url" ], - "title": "Page[Job]" + "title": "PythonCodeFunction" }, - "Page_LicensedItemGet_": { + "PythonCodeFunctionJob": { "properties": { - "items": { - "items": { - "$ref": "#/components/schemas/LicensedItemGet" - }, - "type": "array", - "title": "Items" + "title": { + "type": "string", + "title": "Title", + "default": "" }, - "total": { - "anyOf": [ - { - "type": "integer", - "minimum": 0 - }, - { - "type": "null" - } - ], - "title": "Total" + "description": { + "type": "string", + "title": "Description", + "default": "" }, - "limit": { + "function_uid": { + "type": "string", + "format": "uuid", + "title": "Function Uid" + }, + "inputs": { "anyOf": [ { - "type": "integer", - "minimum": 1 + "type": "object" }, { "type": "null" } ], - "title": "Limit" + "title": "Inputs" }, - "offset": { + "outputs": { "anyOf": [ { - "type": "integer", - "minimum": 0 + "type": "object" }, { "type": "null" } ], - "title": "Offset" + "title": "Outputs" }, - "links": { - "$ref": "#/components/schemas/Links" + "function_class": { + "type": "string", + "const": "python_code", + "title": "Function Class", + "default": "python_code" } }, "type": "object", "required": [ - "items", - "total", - "limit", - "offset", - "links" + "function_uid", + "inputs", + "outputs" ], - "title": "Page[LicensedItemGet]" + "title": "PythonCodeFunctionJob" }, - "Page_Study_": { + "RegisteredFunctionJobCollection": { "properties": { - "items": { + "title": { + "type": "string", + "title": "Title", + "default": "" + }, + "description": { + "type": "string", + "title": "Description", + "default": "" + }, + "job_ids": { "items": { - "$ref": "#/components/schemas/Study" + "type": "string", + "format": "uuid" }, "type": "array", - "title": "Items" + "title": "Job Ids", + "default": [] }, - "total": { - "anyOf": [ - { - "type": "integer", - "minimum": 0 - }, + "uid": { + "type": "string", + "format": "uuid", + "title": "Uid" + } + }, + "type": "object", + "required": [ + "uid" + ], + "title": "RegisteredFunctionJobCollection" + }, + "RegisteredProjectFunction": { + "properties": { + "function_class": { + "type": "string", + "const": "project", + "title": "Function Class", + "default": "project" + }, + "title": { + "type": "string", + "title": "Title", + "default": "" + }, + "description": { + "type": "string", + "title": "Description", + "default": "" + }, + "input_schema": { + "oneOf": [ { - "type": "null" + "$ref": "#/components/schemas/JSONFunctionInputSchema" } ], - "title": "Total" + "title": "Input Schema", + "discriminator": { + "propertyName": "schema_class", + "mapping": { + "application/schema+json": "#/components/schemas/JSONFunctionInputSchema" + } + } }, - "limit": { - "anyOf": [ - { - "type": "integer", - "minimum": 1 - }, + "output_schema": { + "oneOf": [ { - "type": "null" + "$ref": "#/components/schemas/JSONFunctionOutputSchema" } ], - "title": "Limit" + "title": "Output Schema", + "discriminator": { + "propertyName": "schema_class", + "mapping": { + "application/schema+json": "#/components/schemas/JSONFunctionOutputSchema" + } + } }, - "offset": { + "default_inputs": { "anyOf": [ { - "type": "integer", - "minimum": 0 + "type": "object" }, { "type": "null" } ], - "title": "Offset" + "title": "Default Inputs" }, - "links": { - "$ref": "#/components/schemas/Links" + "uid": { + "type": "string", + "format": "uuid", + "title": "Uid" + }, + "project_id": { + "type": "string", + "format": "uuid", + "title": "Project Id" } }, "type": "object", "required": [ - "items", - "total", - "limit", - "offset", - "links" - ], - "title": "Page[Study]" - }, - "PricingPlanClassification": { - "type": "string", - "enum": [ - "TIER", - "LICENSE" + "input_schema", + "output_schema", + "default_inputs", + "uid", + "project_id" ], - "title": "PricingPlanClassification" + "title": "RegisteredProjectFunction" }, - "PricingUnitGetLegacy": { + "RegisteredProjectFunctionJob": { "properties": { - "pricingUnitId": { - "type": "integer", - "exclusiveMinimum": true, - "title": "Pricingunitid", - "minimum": 0 - }, - "unitName": { + "title": { "type": "string", - "title": "Unitname" + "title": "Title", + "default": "" }, - "unitExtraInfo": { - "$ref": "#/components/schemas/UnitExtraInfoTier" + "description": { + "type": "string", + "title": "Description", + "default": "" }, - "currentCostPerUnit": { - "type": "number", - "minimum": 0.0, - "title": "Currentcostperunit" + "function_uid": { + "type": "string", + "format": "uuid", + "title": "Function Uid" }, - "default": { - "type": "boolean", - "title": "Default" - } - }, - "type": "object", - "required": [ - "pricingUnitId", - "unitName", - "unitExtraInfo", - "currentCostPerUnit", - "default" - ], - "title": "PricingUnitGetLegacy" - }, - "Profile": { - "properties": { - "first_name": { + "inputs": { "anyOf": [ { - "type": "string", - "maxLength": 255 + "type": "object" }, { "type": "null" } ], - "title": "First Name" + "title": "Inputs" }, - "last_name": { + "outputs": { "anyOf": [ { - "type": "string", - "maxLength": 255 + "type": "object" }, { "type": "null" } ], - "title": "Last Name" + "title": "Outputs" }, - "id": { - "type": "integer", - "exclusiveMinimum": true, - "title": "Id", - "minimum": 0 + "function_class": { + "type": "string", + "const": "project", + "title": "Function Class", + "default": "project" }, - "login": { + "uid": { "type": "string", - "format": "email", - "title": "Login" + "format": "uuid", + "title": "Uid" }, - "role": { - "$ref": "#/components/schemas/UserRoleEnum" + "project_job_id": { + "type": "string", + "format": "uuid", + "title": "Project Job Id" + } + }, + "type": "object", + "required": [ + "function_uid", + "inputs", + "outputs", + "uid", + "project_job_id" + ], + "title": "RegisteredProjectFunctionJob" + }, + "RegisteredPythonCodeFunction": { + "properties": { + "function_class": { + "type": "string", + "const": "python_code", + "title": "Function Class", + "default": "python_code" }, - "groups": { - "anyOf": [ + "title": { + "type": "string", + "title": "Title", + "default": "" + }, + "description": { + "type": "string", + "title": "Description", + "default": "" + }, + "input_schema": { + "oneOf": [ { - "$ref": "#/components/schemas/Groups" - }, + "$ref": "#/components/schemas/JSONFunctionInputSchema" + } + ], + "title": "Input Schema", + "discriminator": { + "propertyName": "schema_class", + "mapping": { + "application/schema+json": "#/components/schemas/JSONFunctionInputSchema" + } + } + }, + "output_schema": { + "oneOf": [ { - "type": "null" + "$ref": "#/components/schemas/JSONFunctionOutputSchema" } - ] + ], + "title": "Output Schema", + "discriminator": { + "propertyName": "schema_class", + "mapping": { + "application/schema+json": "#/components/schemas/JSONFunctionOutputSchema" + } + } }, - "gravatar_id": { + "default_inputs": { "anyOf": [ { - "type": "string", - "maxLength": 40 + "type": "object" }, { "type": "null" } ], - "title": "Gravatar Id", - "description": "md5 hash value of email to retrieve an avatar image from https://www.gravatar.com" + "title": "Default Inputs" + }, + "uid": { + "type": "string", + "format": "uuid", + "title": "Uid" + }, + "code_url": { + "type": "string", + "title": "Code Url" } }, "type": "object", "required": [ - "id", - "login", - "role" + "input_schema", + "output_schema", + "default_inputs", + "uid", + "code_url" ], - "title": "Profile", - "example": { - "first_name": "James", - "gravatar_id": "9a8930a5b20d7048e37740bac5c1ca4f", - "groups": { - "all": { - "description": "all users", - "gid": "1", - "label": "Everyone" - }, - "me": { - "description": "primary group", - "gid": "123", - "label": "maxy" - }, - "organizations": [] - }, - "id": "20", - "last_name": "Maxwell", - "login": "james-maxwell@itis.swiss", - "role": "USER" - } + "title": "RegisteredPythonCodeFunction" }, - "ProfileUpdate": { + "RegisteredPythonCodeFunctionJob": { "properties": { - "first_name": { + "title": { + "type": "string", + "title": "Title", + "default": "" + }, + "description": { + "type": "string", + "title": "Description", + "default": "" + }, + "function_uid": { + "type": "string", + "format": "uuid", + "title": "Function Uid" + }, + "inputs": { "anyOf": [ { - "type": "string", - "maxLength": 255 + "type": "object" }, { "type": "null" } ], - "title": "First Name" + "title": "Inputs" }, - "last_name": { + "outputs": { "anyOf": [ { - "type": "string", - "maxLength": 255 + "type": "object" }, { "type": "null" } ], - "title": "Last Name" + "title": "Outputs" + }, + "function_class": { + "type": "string", + "const": "python_code", + "title": "Function Class", + "default": "python_code" + }, + "uid": { + "type": "string", + "format": "uuid", + "title": "Uid" } }, "type": "object", - "title": "ProfileUpdate" + "required": [ + "function_uid", + "inputs", + "outputs", + "uid" + ], + "title": "RegisteredPythonCodeFunctionJob" }, - "Program": { + "RegisteredSolverFunction": { "properties": { - "id": { - "type": "string", - "title": "Id", - "description": "Resource identifier" - }, - "version": { + "function_class": { "type": "string", - "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$", - "title": "Version", - "description": "Semantic version number of the resource" + "const": "solver", + "title": "Function Class", + "default": "solver" }, "title": { "type": "string", - "maxLength": 100, "title": "Title", - "description": "Human readable name" + "default": "" }, "description": { + "type": "string", + "title": "Description", + "default": "" + }, + "input_schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/JSONFunctionInputSchema" + } + ], + "title": "Input Schema", + "discriminator": { + "propertyName": "schema_class", + "mapping": { + "application/schema+json": "#/components/schemas/JSONFunctionInputSchema" + } + } + }, + "output_schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/JSONFunctionOutputSchema" + } + ], + "title": "Output Schema", + "discriminator": { + "propertyName": "schema_class", + "mapping": { + "application/schema+json": "#/components/schemas/JSONFunctionOutputSchema" + } + } + }, + "default_inputs": { "anyOf": [ { - "type": "string", - "maxLength": 1000 + "type": "object" }, { "type": "null" } ], + "title": "Default Inputs" + }, + "uid": { + "type": "string", + "format": "uuid", + "title": "Uid" + }, + "solver_key": { + "type": "string", + "pattern": "^simcore/services/((comp|dynamic|frontend))/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$", + "title": "Solver Key" + }, + "solver_version": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$", + "title": "Solver Version" + } + }, + "type": "object", + "required": [ + "input_schema", + "output_schema", + "default_inputs", + "uid", + "solver_key", + "solver_version" + ], + "title": "RegisteredSolverFunction" + }, + "RegisteredSolverFunctionJob": { + "properties": { + "title": { + "type": "string", + "title": "Title", + "default": "" + }, + "description": { + "type": "string", "title": "Description", - "description": "Description of the resource" + "default": "" }, - "url": { + "function_uid": { + "type": "string", + "format": "uuid", + "title": "Function Uid" + }, + "inputs": { "anyOf": [ { - "type": "string", - "maxLength": 2083, - "minLength": 1, - "format": "uri" + "type": "object" }, { "type": "null" } ], - "title": "Url", - "description": "Link to get this resource" + "title": "Inputs" }, - "version_display": { + "outputs": { "anyOf": [ { - "type": "string" + "type": "object" }, { "type": "null" } ], - "title": "Version Display" + "title": "Outputs" + }, + "function_class": { + "type": "string", + "const": "solver", + "title": "Function Class", + "default": "solver" + }, + "uid": { + "type": "string", + "format": "uuid", + "title": "Uid" + }, + "solver_job_id": { + "type": "string", + "format": "uuid", + "title": "Solver Job Id" } }, "type": "object", "required": [ - "id", - "version", - "title", - "url", - "version_display" + "function_uid", + "inputs", + "outputs", + "uid", + "solver_job_id" ], - "title": "Program", - "description": "A released program with a specific version", - "example": { - "description": "Simulation framework", - "id": "simcore/services/dynamic/sim4life", - "maintainer": "info@itis.swiss", - "title": "Sim4life", - "url": "https://api.osparc.io/v0/solvers/simcore%2Fservices%2Fdynamic%2Fsim4life/releases/8.0.0", - "version": "8.0.0", - "version_display": "8.0.0" - } + "title": "RegisteredSolverFunctionJob" }, "RunningState": { "type": "string", @@ -7924,6 +10322,144 @@ "version": "2.1.1" } }, + "SolverFunction": { + "properties": { + "function_class": { + "type": "string", + "const": "solver", + "title": "Function Class", + "default": "solver" + }, + "title": { + "type": "string", + "title": "Title", + "default": "" + }, + "description": { + "type": "string", + "title": "Description", + "default": "" + }, + "input_schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/JSONFunctionInputSchema" + } + ], + "title": "Input Schema", + "discriminator": { + "propertyName": "schema_class", + "mapping": { + "application/schema+json": "#/components/schemas/JSONFunctionInputSchema" + } + } + }, + "output_schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/JSONFunctionOutputSchema" + } + ], + "title": "Output Schema", + "discriminator": { + "propertyName": "schema_class", + "mapping": { + "application/schema+json": "#/components/schemas/JSONFunctionOutputSchema" + } + } + }, + "default_inputs": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Default Inputs" + }, + "solver_key": { + "type": "string", + "pattern": "^simcore/services/((comp|dynamic|frontend))/([a-z0-9][a-z0-9_.-]*/)*([a-z0-9-_]+[a-z0-9])$", + "title": "Solver Key" + }, + "solver_version": { + "type": "string", + "pattern": "^(0|[1-9]\\d*)(\\.(0|[1-9]\\d*)){2}(-(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*)(\\.(0|[1-9]\\d*|\\d*[-a-zA-Z][-\\da-zA-Z]*))*)?(\\+[-\\da-zA-Z]+(\\.[-\\da-zA-Z-]+)*)?$", + "title": "Solver Version" + } + }, + "type": "object", + "required": [ + "input_schema", + "output_schema", + "default_inputs", + "solver_key", + "solver_version" + ], + "title": "SolverFunction" + }, + "SolverFunctionJob": { + "properties": { + "title": { + "type": "string", + "title": "Title", + "default": "" + }, + "description": { + "type": "string", + "title": "Description", + "default": "" + }, + "function_uid": { + "type": "string", + "format": "uuid", + "title": "Function Uid" + }, + "inputs": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Inputs" + }, + "outputs": { + "anyOf": [ + { + "type": "object" + }, + { + "type": "null" + } + ], + "title": "Outputs" + }, + "function_class": { + "type": "string", + "const": "solver", + "title": "Function Class", + "default": "solver" + }, + "solver_job_id": { + "type": "string", + "format": "uuid", + "title": "Solver Job Id" + } + }, + "type": "object", + "required": [ + "function_uid", + "inputs", + "outputs", + "solver_job_id" + ], + "title": "SolverFunctionJob" + }, "SolverPort": { "properties": { "key": { diff --git a/services/api-server/requirements/_test.in b/services/api-server/requirements/_test.in index 718feaeb205..805e1f7a7af 100644 --- a/services/api-server/requirements/_test.in +++ b/services/api-server/requirements/_test.in @@ -31,3 +31,4 @@ respx sqlalchemy[mypy] # adds Mypy / Pep-484 Support for ORM Mappings SEE https://docs.sqlalchemy.org/en/20/orm/extensions/mypy.html types-aiofiles types-boto3 +types-jsonschema diff --git a/services/api-server/requirements/_test.txt b/services/api-server/requirements/_test.txt index 957df747187..8ce05a958cb 100644 --- a/services/api-server/requirements/_test.txt +++ b/services/api-server/requirements/_test.txt @@ -359,6 +359,8 @@ types-awscrt==0.23.10 # via botocore-stubs types-boto3==1.37.4 # via -r requirements/_test.in +types-jsonschema==4.23.0.20241208 + # via -r requirements/_test.in types-s3transfer==0.11.3 # via types-boto3 typing-extensions==4.12.2 diff --git a/services/api-server/src/simcore_service_api_server/api/root.py b/services/api-server/src/simcore_service_api_server/api/root.py index 5654601d403..5a1de4a711c 100644 --- a/services/api-server/src/simcore_service_api_server/api/root.py +++ b/services/api-server/src/simcore_service_api_server/api/root.py @@ -6,7 +6,7 @@ from .routes import credits as _credits from .routes import ( files, - functions, + functions_routes, health, licensed_items, meta, @@ -42,12 +42,27 @@ def create_router(settings: ApplicationSettings): ) router.include_router(studies.router, tags=["studies"], prefix="/studies") router.include_router(studies_jobs.router, tags=["studies"], prefix="/studies") + router.include_router( + functions_routes.function_router, tags=["functions"], prefix="/functions" + ) + router.include_router( + functions_routes.function_job_router, + tags=["function_jobs"], + prefix="/function_jobs", + ) + router.include_router( + functions_routes.function_job_collections_router, + tags=["function_job_collections"], + prefix="/function_job_collections", + ) router.include_router(wallets.router, tags=["wallets"], prefix="/wallets") router.include_router(_credits.router, tags=["credits"], prefix="/credits") router.include_router( licensed_items.router, tags=["licensed-items"], prefix="/licensed-items" ) - router.include_router(functions.router, tags=["functions"], prefix="/functions") + router.include_router( + functions_routes.function_router, tags=["functions"], prefix="/functions" + ) # NOTE: multiple-files upload is currently disabled # Web form to upload files at http://localhost:8000/v0/upload-form-view diff --git a/services/api-server/src/simcore_service_api_server/api/routes/functions.py b/services/api-server/src/simcore_service_api_server/api/routes/functions.py deleted file mode 100644 index 6d5c277451d..00000000000 --- a/services/api-server/src/simcore_service_api_server/api/routes/functions.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import Annotated - -from fastapi import APIRouter, Depends - -from ...services_rpc.wb_api_server import WbApiRpcClient -from ..dependencies.webserver_rpc import ( - get_wb_api_rpc_client, -) - -router = APIRouter() - - -@router.post("/ping", include_in_schema=False) -async def ping( - wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], -): - return await wb_api_rpc.ping() diff --git a/services/api-server/src/simcore_service_api_server/api/routes/functions_routes.py b/services/api-server/src/simcore_service_api_server/api/routes/functions_routes.py new file mode 100644 index 00000000000..5c4643d2e06 --- /dev/null +++ b/services/api-server/src/simcore_service_api_server/api/routes/functions_routes.py @@ -0,0 +1,659 @@ +import asyncio +from collections.abc import Callable +from typing import Annotated, Final + +import jsonschema +from fastapi import APIRouter, Depends, Request, status +from fastapi_pagination.api import create_page +from jsonschema import ValidationError +from models_library.api_schemas_webserver.functions_wb_schema import ( + Function, + FunctionClass, + FunctionID, + FunctionInputs, + FunctionInputSchema, + FunctionInputsList, + FunctionInputsValidationError, + FunctionJob, + FunctionJobCollection, + FunctionJobCollectionID, + FunctionJobCollectionStatus, + FunctionJobID, + FunctionJobStatus, + FunctionOutputs, + FunctionOutputSchema, + FunctionSchemaClass, + ProjectFunctionJob, + RegisteredFunction, + RegisteredFunctionJob, + RegisteredFunctionJobCollection, + SolverFunctionJob, + UnsupportedFunctionClassError, + UnsupportedFunctionFunctionJobClassCombinationError, +) +from pydantic import PositiveInt +from servicelib.fastapi.dependencies import get_reverse_url_mapper +from simcore_service_api_server._service_jobs import JobService +from sqlalchemy.ext.asyncio import AsyncEngine + +from ..._service_solvers import SolverService +from ...models.pagination import Page, PaginationParams +from ...models.schemas.errors import ErrorGet +from ...models.schemas.jobs import JobInputs +from ...services_http.director_v2 import DirectorV2Api +from ...services_http.storage import StorageApi +from ...services_http.webserver import AuthSession +from ...services_rpc.wb_api_server import WbApiRpcClient +from ..dependencies.authentication import get_current_user_id, get_product_name +from ..dependencies.database import get_db_asyncpg_engine +from ..dependencies.services import get_api_client, get_job_service, get_solver_service +from ..dependencies.webserver_http import get_webserver_session +from ..dependencies.webserver_rpc import get_wb_api_rpc_client +from . import solvers_jobs, solvers_jobs_getters, studies_jobs + +# pylint: disable=too-many-arguments,no-else-return + +function_router = APIRouter() +function_job_router = APIRouter() +function_job_collections_router = APIRouter() + +_COMMON_FUNCTION_ERROR_RESPONSES: Final[dict] = { + status.HTTP_404_NOT_FOUND: { + "description": "Function not found", + "model": ErrorGet, + }, +} + + +@function_router.post( + "", + response_model=RegisteredFunction, + responses={**_COMMON_FUNCTION_ERROR_RESPONSES}, + description="Create function", +) +async def register_function( + wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], + function: Function, +) -> RegisteredFunction: + return await wb_api_rpc.register_function(function=function) + + +@function_router.get( + "/{function_id:uuid}", + response_model=RegisteredFunction, + responses={**_COMMON_FUNCTION_ERROR_RESPONSES}, + description="Get function", +) +async def get_function( + function_id: FunctionID, + wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], +) -> RegisteredFunction: + return await wb_api_rpc.get_function(function_id=function_id) + + +@function_router.get( + "", response_model=Page[RegisteredFunction], description="List functions" +) +async def list_functions( + wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], + page_params: Annotated[PaginationParams, Depends()], +): + functions_list, meta = await wb_api_rpc.list_functions( + pagination_offset=page_params.offset, + pagination_limit=page_params.limit, + ) + + return create_page( + functions_list, + total=meta.total, + params=page_params, + ) + + +@function_job_router.get( + "", response_model=Page[RegisteredFunctionJob], description="List function jobs" +) +async def list_function_jobs( + wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], + page_params: Annotated[PaginationParams, Depends()], +): + function_jobs_list, meta = await wb_api_rpc.list_function_jobs( + pagination_offset=page_params.offset, + pagination_limit=page_params.limit, + ) + + return create_page( + function_jobs_list, + total=meta.total, + params=page_params, + ) + + +@function_job_collections_router.get( + "", + response_model=Page[RegisteredFunctionJobCollection], + description="List function job collections", +) +async def list_function_job_collections( + wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], + page_params: Annotated[PaginationParams, Depends()], +): + function_job_collection_list, meta = await wb_api_rpc.list_function_job_collections( + pagination_offset=page_params.offset, + pagination_limit=page_params.limit, + ) + return create_page( + function_job_collection_list, + total=meta.total, + params=page_params, + ) + + +def _join_inputs( + default_inputs: FunctionInputs | None, + function_inputs: FunctionInputs | None, +) -> FunctionInputs: + if default_inputs is None: + return function_inputs + + if function_inputs is None: + return default_inputs + + # last dict will override defaults + return {**default_inputs, **function_inputs} + + +@function_router.get( + "/{function_id:uuid}/input_schema", + response_model=FunctionInputSchema, + responses={**_COMMON_FUNCTION_ERROR_RESPONSES}, + description="Get function input schema", +) +async def get_function_inputschema( + function_id: FunctionID, + wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], +) -> FunctionInputSchema: + function = await wb_api_rpc.get_function(function_id=function_id) + return function.input_schema + + +@function_router.get( + "/{function_id:uuid}/output_schema", + response_model=FunctionInputSchema, + responses={**_COMMON_FUNCTION_ERROR_RESPONSES}, + description="Get function input schema", +) +async def get_function_outputschema( + function_id: FunctionID, + wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], +) -> FunctionOutputSchema: + function = await wb_api_rpc.get_function(function_id=function_id) + return function.output_schema + + +@function_router.post( + "/{function_id:uuid}:validate_inputs", + response_model=tuple[bool, str], + responses={ + status.HTTP_400_BAD_REQUEST: {"description": "Invalid inputs"}, + status.HTTP_404_NOT_FOUND: {"description": "Function not found"}, + }, + description="Validate inputs against the function's input schema", +) +async def validate_function_inputs( + function_id: FunctionID, + inputs: FunctionInputs, + wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], +) -> tuple[bool, str]: + function = await wb_api_rpc.get_function(function_id=function_id) + + if function.input_schema is None or function.input_schema.schema_content is None: + return True, "No input schema defined for this function" + + if function.input_schema.schema_class == FunctionSchemaClass.json_schema: + try: + jsonschema.validate( + instance=inputs, schema=function.input_schema.schema_content + ) + except ValidationError as err: + return False, str(err) + return True, "Inputs are valid" + + return ( + False, + f"Unsupported function schema class {function.input_schema.schema_class}", + ) + + +@function_router.post( + "/{function_id:uuid}:run", + response_model=RegisteredFunctionJob, + responses={**_COMMON_FUNCTION_ERROR_RESPONSES}, + description="Run function", +) +async def run_function( # noqa: PLR0913 + request: Request, + wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], + webserver_api: Annotated[AuthSession, Depends(get_webserver_session)], + url_for: Annotated[Callable, Depends(get_reverse_url_mapper)], + director2_api: Annotated[DirectorV2Api, Depends(get_api_client(DirectorV2Api))], + function_id: FunctionID, + function_inputs: FunctionInputs, + user_id: Annotated[PositiveInt, Depends(get_current_user_id)], + product_name: Annotated[str, Depends(get_product_name)], + solver_service: Annotated[SolverService, Depends(get_solver_service)], + job_service: Annotated[JobService, Depends(get_job_service)], +) -> RegisteredFunctionJob: + + to_run_function = await wb_api_rpc.get_function(function_id=function_id) + + joined_inputs = _join_inputs( + to_run_function.default_inputs, + function_inputs, + ) + + if to_run_function.input_schema is not None: + is_valid, validation_str = await validate_function_inputs( + function_id=to_run_function.uid, + inputs=joined_inputs, + wb_api_rpc=wb_api_rpc, + ) + if not is_valid: + raise FunctionInputsValidationError(error=validation_str) + + if cached_function_job := await wb_api_rpc.find_cached_function_job( + function_id=to_run_function.uid, + inputs=joined_inputs, + ): + return cached_function_job + + if to_run_function.function_class == FunctionClass.project: + study_job = await studies_jobs.create_study_job( + study_id=to_run_function.project_id, + job_inputs=JobInputs(values=joined_inputs or {}), + webserver_api=webserver_api, + wb_api_rpc=wb_api_rpc, + url_for=url_for, + x_simcore_parent_project_uuid=None, + x_simcore_parent_node_id=None, + user_id=user_id, + product_name=product_name, + ) + await studies_jobs.start_study_job( + request=request, + study_id=to_run_function.project_id, + job_id=study_job.id, + user_id=user_id, + webserver_api=webserver_api, + director2_api=director2_api, + ) + return await register_function_job( + wb_api_rpc=wb_api_rpc, + function_job=ProjectFunctionJob( + function_uid=to_run_function.uid, + title=f"Function job of function {to_run_function.uid}", + description=to_run_function.description, + inputs=joined_inputs, + outputs=None, + project_job_id=study_job.id, + ), + ) + elif to_run_function.function_class == FunctionClass.solver: # noqa: RET505 + solver_job = await solvers_jobs.create_solver_job( + solver_key=to_run_function.solver_key, + version=to_run_function.solver_version, + inputs=JobInputs(values=joined_inputs or {}), + solver_service=solver_service, + job_service=job_service, + url_for=url_for, + x_simcore_parent_project_uuid=None, + x_simcore_parent_node_id=None, + ) + await solvers_jobs.start_job( + request=request, + solver_key=to_run_function.solver_key, + version=to_run_function.solver_version, + job_id=solver_job.id, + user_id=user_id, + webserver_api=webserver_api, + director2_api=director2_api, + ) + return await register_function_job( + wb_api_rpc=wb_api_rpc, + function_job=SolverFunctionJob( + function_uid=to_run_function.uid, + title=f"Function job of function {to_run_function.uid}", + description=to_run_function.description, + inputs=joined_inputs, + outputs=None, + solver_job_id=solver_job.id, + ), + ) + else: + raise UnsupportedFunctionClassError( + function_class=to_run_function.function_class, + ) + + +@function_router.delete( + "/{function_id:uuid}", + response_model=None, + responses={**_COMMON_FUNCTION_ERROR_RESPONSES}, + description="Delete function", +) +async def delete_function( + wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], + function_id: FunctionID, +) -> None: + return await wb_api_rpc.delete_function(function_id=function_id) + + +_COMMON_FUNCTION_JOB_ERROR_RESPONSES: Final[dict] = { + status.HTTP_404_NOT_FOUND: { + "description": "Function job not found", + "model": ErrorGet, + }, +} + + +@function_job_router.post( + "", response_model=RegisteredFunctionJob, description="Create function job" +) +async def register_function_job( + function_job: FunctionJob, + wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], +) -> RegisteredFunctionJob: + return await wb_api_rpc.register_function_job(function_job=function_job) + + +@function_job_router.get( + "/{function_job_id:uuid}", + response_model=RegisteredFunctionJob, + responses={**_COMMON_FUNCTION_JOB_ERROR_RESPONSES}, + description="Get function job", +) +async def get_function_job( + function_job_id: FunctionJobID, + wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], +) -> RegisteredFunctionJob: + return await wb_api_rpc.get_function_job(function_job_id=function_job_id) + + +@function_job_router.delete( + "/{function_job_id:uuid}", + response_model=None, + responses={**_COMMON_FUNCTION_JOB_ERROR_RESPONSES}, + description="Delete function job", +) +async def delete_function_job( + function_job_id: FunctionJobID, + wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], +) -> None: + return await wb_api_rpc.delete_function_job(function_job_id=function_job_id) + + +async def get_function_from_functionjobid( + wb_api_rpc: WbApiRpcClient, + function_job_id: FunctionJobID, +) -> tuple[Function, FunctionJob]: + function_job = await get_function_job( + wb_api_rpc=wb_api_rpc, function_job_id=function_job_id + ) + + return ( + await get_function( + wb_api_rpc=wb_api_rpc, function_id=function_job.function_uid + ), + function_job, + ) + + +@function_job_router.get( + "/{function_job_id:uuid}/status", + response_model=FunctionJobStatus, + responses={**_COMMON_FUNCTION_JOB_ERROR_RESPONSES}, + description="Get function job status", +) +async def function_job_status( + function_job_id: FunctionJobID, + director2_api: Annotated[DirectorV2Api, Depends(get_api_client(DirectorV2Api))], + user_id: Annotated[PositiveInt, Depends(get_current_user_id)], + wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], +) -> FunctionJobStatus: + function, function_job = await get_function_from_functionjobid( + wb_api_rpc=wb_api_rpc, function_job_id=function_job_id + ) + + if ( + function.function_class == FunctionClass.project + and function_job.function_class == FunctionClass.project + ): + job_status = await studies_jobs.inspect_study_job( + study_id=function.project_id, + job_id=function_job.project_job_id, + user_id=user_id, + director2_api=director2_api, + ) + return FunctionJobStatus(status=job_status.state) + elif (function.function_class == FunctionClass.solver) and ( # noqa: RET505 + function_job.function_class == FunctionClass.solver + ): + job_status = await solvers_jobs.inspect_job( + solver_key=function.solver_key, + version=function.solver_version, + job_id=function_job.solver_job_id, + user_id=user_id, + director2_api=director2_api, + ) + return FunctionJobStatus(status=job_status.state) + else: + raise UnsupportedFunctionFunctionJobClassCombinationError( + function_class=function.function_class, + function_job_class=function_job.function_class, + ) + + +@function_job_router.get( + "/{function_job_id:uuid}/outputs", + response_model=FunctionOutputs, + responses={**_COMMON_FUNCTION_JOB_ERROR_RESPONSES}, + description="Get function job outputs", +) +async def function_job_outputs( + function_job_id: FunctionJobID, + webserver_api: Annotated[AuthSession, Depends(get_webserver_session)], + user_id: Annotated[PositiveInt, Depends(get_current_user_id)], + storage_client: Annotated[StorageApi, Depends(get_api_client(StorageApi))], + wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], + async_pg_engine: Annotated[AsyncEngine, Depends(get_db_asyncpg_engine)], +) -> FunctionOutputs: + function, function_job = await get_function_from_functionjobid( + wb_api_rpc=wb_api_rpc, function_job_id=function_job_id + ) + + if ( + function.function_class == FunctionClass.project + and function_job.function_class == FunctionClass.project + ): + return ( + await studies_jobs.get_study_job_outputs( + study_id=function.project_id, + job_id=function_job.project_job_id, + user_id=user_id, + webserver_api=webserver_api, + storage_client=storage_client, + ) + ).results + + if ( + function.function_class == FunctionClass.solver + and function_job.function_class == FunctionClass.solver + ): + return ( + await solvers_jobs_getters.get_job_outputs( + solver_key=function.solver_key, + version=function.solver_version, + job_id=function_job.solver_job_id, + user_id=user_id, + webserver_api=webserver_api, + storage_client=storage_client, + async_pg_engine=async_pg_engine, + ) + ).results + raise UnsupportedFunctionClassError(function_class=function.function_class) + + +@function_router.post( + "/{function_id:uuid}:map", + response_model=RegisteredFunctionJobCollection, + responses={**_COMMON_FUNCTION_ERROR_RESPONSES}, + description="Map function over input parameters", +) +async def map_function( # noqa: PLR0913 + function_id: FunctionID, + function_inputs_list: FunctionInputsList, + request: Request, + wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], + webserver_api: Annotated[AuthSession, Depends(get_webserver_session)], + url_for: Annotated[Callable, Depends(get_reverse_url_mapper)], + director2_api: Annotated[DirectorV2Api, Depends(get_api_client(DirectorV2Api))], + user_id: Annotated[PositiveInt, Depends(get_current_user_id)], + product_name: Annotated[str, Depends(get_product_name)], + solver_service: Annotated[SolverService, Depends(get_solver_service)], + job_service: Annotated[JobService, Depends(get_job_service)], +) -> RegisteredFunctionJobCollection: + function_jobs = [] + function_jobs = [ + await run_function( + wb_api_rpc=wb_api_rpc, + function_id=function_id, + function_inputs=function_inputs, + product_name=product_name, + user_id=user_id, + webserver_api=webserver_api, + url_for=url_for, + director2_api=director2_api, + request=request, + solver_service=solver_service, + job_service=job_service, + ) + for function_inputs in function_inputs_list + ] + + function_job_collection_description = f"Function job collection of map of function {function_id} with {len(function_inputs_list)} inputs" + return await register_function_job_collection( + wb_api_rpc=wb_api_rpc, + function_job_collection=FunctionJobCollection( + title="Function job collection of function map", + description=function_job_collection_description, + job_ids=[function_job.uid for function_job in function_jobs], + ), + ) + + +_COMMON_FUNCTION_JOB_COLLECTION_ERROR_RESPONSES: Final[dict] = { + status.HTTP_404_NOT_FOUND: { + "description": "Function job collection not found", + "model": ErrorGet, + }, +} + + +@function_job_collections_router.get( + "/{function_job_collection_id:uuid}", + response_model=RegisteredFunctionJobCollection, + responses={**_COMMON_FUNCTION_JOB_COLLECTION_ERROR_RESPONSES}, + description="Get function job collection", +) +async def get_function_job_collection( + function_job_collection_id: FunctionJobCollectionID, + wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], +) -> RegisteredFunctionJobCollection: + return await wb_api_rpc.get_function_job_collection( + function_job_collection_id=function_job_collection_id + ) + + +@function_job_collections_router.post( + "", + response_model=RegisteredFunctionJobCollection, + description="Register function job collection", +) +async def register_function_job_collection( + function_job_collection: FunctionJobCollection, + wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], +) -> RegisteredFunctionJobCollection: + return await wb_api_rpc.register_function_job_collection( + function_job_collection=function_job_collection + ) + + +@function_job_collections_router.delete( + "/{function_job_collection_id:uuid}", + response_model=None, + responses={**_COMMON_FUNCTION_JOB_COLLECTION_ERROR_RESPONSES}, + description="Delete function job collection", +) +async def delete_function_job_collection( + function_job_collection_id: FunctionJobCollectionID, + wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], +) -> None: + return await wb_api_rpc.delete_function_job_collection( + function_job_collection_id=function_job_collection_id + ) + + +@function_job_collections_router.get( + "/{function_job_collection_id:uuid}/function_jobs", + response_model=list[RegisteredFunctionJob], + responses={**_COMMON_FUNCTION_JOB_COLLECTION_ERROR_RESPONSES}, + description="Get the function jobs in function job collection", +) +async def function_job_collection_list_function_jobs( + function_job_collection_id: FunctionJobCollectionID, + wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], +) -> list[RegisteredFunctionJob]: + function_job_collection = await get_function_job_collection( + function_job_collection_id=function_job_collection_id, + wb_api_rpc=wb_api_rpc, + ) + return [ + await get_function_job( + job_id, + wb_api_rpc=wb_api_rpc, + ) + for job_id in function_job_collection.job_ids + ] + + +@function_job_collections_router.get( + "/{function_job_collection_id:uuid}/status", + response_model=FunctionJobCollectionStatus, + responses={**_COMMON_FUNCTION_JOB_COLLECTION_ERROR_RESPONSES}, + description="Get function job collection status", +) +async def function_job_collection_status( + function_job_collection_id: FunctionJobCollectionID, + wb_api_rpc: Annotated[WbApiRpcClient, Depends(get_wb_api_rpc_client)], + director2_api: Annotated[DirectorV2Api, Depends(get_api_client(DirectorV2Api))], + user_id: Annotated[PositiveInt, Depends(get_current_user_id)], +) -> FunctionJobCollectionStatus: + function_job_collection = await get_function_job_collection( + function_job_collection_id=function_job_collection_id, + wb_api_rpc=wb_api_rpc, + ) + + job_statuses = await asyncio.gather( + *[ + function_job_status( + job_id, + wb_api_rpc=wb_api_rpc, + director2_api=director2_api, + user_id=user_id, + ) + for job_id in function_job_collection.job_ids + ] + ) + return FunctionJobCollectionStatus( + status=[job_status.status for job_status in job_statuses] + ) diff --git a/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py b/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py index 7b724de24dc..4490db6e628 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/solvers_jobs.py @@ -86,7 +86,7 @@ def compose_job_resource_name(solver_key, solver_version, job_id) -> str: status_code=status.HTTP_201_CREATED, responses=JOBS_STATUS_CODES, ) -async def create_solver_job( +async def create_solver_job( # noqa: PLR0913 solver_key: SolverKeyId, version: VersionStr, inputs: JobInputs, diff --git a/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py b/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py index 436633b1c18..0da6c51574c 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py @@ -133,7 +133,7 @@ async def create_study_job( url_for: Annotated[Callable, Depends(get_reverse_url_mapper)], user_id: Annotated[PositiveInt, Depends(get_current_user_id)], product_name: Annotated[str, Depends(get_product_name)], - hidden: Annotated[bool, Query()] = True, + hidden: Annotated[bool, Query()] = True, # noqa: FBT002 x_simcore_parent_project_uuid: ProjectID | None = Header(default=None), x_simcore_parent_node_id: NodeID | None = Header(default=None), ) -> Job: @@ -304,13 +304,12 @@ async def start_study_job( return JSONResponse( content=jsonable_encoder(job_status), status_code=status.HTTP_200_OK ) - job_status = await inspect_study_job( + return await inspect_study_job( study_id=study_id, job_id=job_id, user_id=user_id, director2_api=director2_api, ) - return job_status @router.post( @@ -387,10 +386,9 @@ async def get_study_job_output_logfile( level=logging.DEBUG, msg=f"get study job output logfile study_id={study_id!r} job_id={job_id!r}.", ): - log_link_map = await director2_api.get_computation_logs( + return await director2_api.get_computation_logs( user_id=user_id, project_id=job_id ) - return log_link_map @router.get( diff --git a/services/api-server/src/simcore_service_api_server/models/schemas/functions_api_schema.py b/services/api-server/src/simcore_service_api_server/models/schemas/functions_api_schema.py new file mode 100644 index 00000000000..ba316c0e88d --- /dev/null +++ b/services/api-server/src/simcore_service_api_server/models/schemas/functions_api_schema.py @@ -0,0 +1,85 @@ +from enum import Enum +from typing import Annotated, Any, Literal, TypeAlias + +from models_library import projects +from pydantic import BaseModel, Field + +FunctionID: TypeAlias = projects.ProjectID + + +class FunctionSchema(BaseModel): + schema_dict: dict[str, Any] | None # JSON Schema + + +class FunctionInputSchema(FunctionSchema): ... + + +class FunctionOutputSchema(FunctionSchema): ... + + +class FunctionClass(str, Enum): + project = "project" + python_code = "python_code" + + +class FunctionInputs(BaseModel): + inputs_dict: dict[str, Any] | None # JSON Schema + + +class FunctionOutputs(BaseModel): + outputs_dict: dict[str, Any] | None # JSON Schema + + +class Function(BaseModel): + uid: FunctionID | None = None + title: str | None = None + description: str | None = None + input_schema: FunctionInputSchema | None = None + output_schema: FunctionOutputSchema | None = None + + +class StudyFunction(Function): + function_type: Literal["study"] = "study" + study_url: str + + +class PythonCodeFunction(Function): + function_type: Literal["python_code"] = "python_code" + code_url: str + + +FunctionUnion: TypeAlias = Annotated[ + StudyFunction | PythonCodeFunction, + Field(discriminator="function_type"), +] + +FunctionJobID: TypeAlias = projects.ProjectID +FunctionJobCollectionID: TypeAlias = projects.ProjectID + + +class FunctionJob(BaseModel): + uid: FunctionJobID + title: str | None + description: str | None + status: str + function_uid: FunctionID + inputs: FunctionInputs | None + outputs: FunctionOutputs | None + + +class FunctionJobStatus(BaseModel): + status: str + + +class FunctionJobCollection(BaseModel): + """Model for a collection of function jobs""" + + id: FunctionJobCollectionID + title: str | None + description: str | None + job_ids: list[FunctionJobID] + status: str + + +class FunctionJobCollectionStatus(BaseModel): + status: list[str] diff --git a/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py index fa9da284649..32ba29edeb6 100644 --- a/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py +++ b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py @@ -4,6 +4,20 @@ from fastapi import FastAPI from fastapi_pagination import create_page +from models_library.api_schemas_webserver.functions_wb_schema import ( + Function, + FunctionID, + FunctionInputs, + FunctionInputSchema, + FunctionJob, + FunctionJobCollection, + FunctionJobCollectionID, + FunctionJobID, + FunctionOutputSchema, + RegisteredFunction, + RegisteredFunctionJob, + RegisteredFunctionJobCollection, +) from models_library.api_schemas_webserver.licensed_items import LicensedItemRpcGetPage from models_library.licenses import LicensedItemID from models_library.products import ProductName @@ -11,6 +25,12 @@ from models_library.resource_tracker_licensed_items_checkouts import ( LicensedItemCheckoutID, ) +from models_library.rest_pagination import ( + DEFAULT_NUMBER_OF_ITEMS_PER_PAGE, + PageLimitInt, + PageMetaInfoLimitOffset, + PageOffsetInt, +) from models_library.services_types import ServiceRunID from models_library.users import UserID from models_library.wallets import WalletID @@ -29,8 +49,8 @@ NotEnoughAvailableSeatsError, ) from servicelib.rabbitmq.rpc_interfaces.webserver import projects as projects_rpc -from servicelib.rabbitmq.rpc_interfaces.webserver.functions.functions import ( - ping as _ping, +from servicelib.rabbitmq.rpc_interfaces.webserver.functions import ( + functions_rpc_interface, ) from servicelib.rabbitmq.rpc_interfaces.webserver.licenses.licensed_items import ( checkout_licensed_item_for_wallet as _checkout_licensed_item_for_wallet, @@ -59,6 +79,8 @@ LicensedResource, ) +# pylint: disable=too-many-public-methods + _exception_mapper = partial(service_exception_mapper, service_name="WebApiServer") @@ -201,9 +223,6 @@ async def release_licensed_item_for_wallet( num_of_seats=licensed_item_checkout_get.num_of_seats, ) - async def ping(self) -> str: - return await _ping(self._client) - async def mark_project_as_job( self, product_name: ProductName, @@ -237,6 +256,136 @@ async def list_projects_marked_as_jobs( job_parent_resource_name_prefix=job_parent_resource_name_prefix, ) + async def register_function(self, *, function: Function) -> RegisteredFunction: + return await functions_rpc_interface.register_function( + self._client, + function=function, + ) + + async def get_function(self, *, function_id: FunctionID) -> RegisteredFunction: + return await functions_rpc_interface.get_function( + self._client, + function_id=function_id, + ) + + async def delete_function(self, *, function_id: FunctionID) -> None: + return await functions_rpc_interface.delete_function( + self._client, + function_id=function_id, + ) + + async def list_functions( + self, + *, + pagination_offset: PageOffsetInt = 0, + pagination_limit: PageLimitInt = DEFAULT_NUMBER_OF_ITEMS_PER_PAGE, + ) -> tuple[list[RegisteredFunction], PageMetaInfoLimitOffset]: + + return await functions_rpc_interface.list_functions( + self._client, + pagination_offset=pagination_offset, + pagination_limit=pagination_limit, + ) + + async def list_function_jobs( + self, + *, + pagination_offset: PageOffsetInt = 0, + pagination_limit: PageLimitInt = DEFAULT_NUMBER_OF_ITEMS_PER_PAGE, + ) -> tuple[list[RegisteredFunctionJob], PageMetaInfoLimitOffset]: + return await functions_rpc_interface.list_function_jobs( + self._client, + pagination_offset=pagination_offset, + pagination_limit=pagination_limit, + ) + + async def list_function_job_collections( + self, + *, + pagination_offset: PageOffsetInt = 0, + pagination_limit: PageLimitInt = DEFAULT_NUMBER_OF_ITEMS_PER_PAGE, + ) -> tuple[list[RegisteredFunctionJobCollection], PageMetaInfoLimitOffset]: + return await functions_rpc_interface.list_function_job_collections( + self._client, + pagination_offset=pagination_offset, + pagination_limit=pagination_limit, + ) + + async def run_function( + self, *, function_id: FunctionID, inputs: FunctionInputs + ) -> RegisteredFunctionJob: + return await functions_rpc_interface.run_function( + self._client, + function_id=function_id, + inputs=inputs, + ) + + async def get_function_job( + self, *, function_job_id: FunctionJobID + ) -> RegisteredFunctionJob: + return await functions_rpc_interface.get_function_job( + self._client, + function_job_id=function_job_id, + ) + + async def delete_function_job(self, *, function_job_id: FunctionJobID) -> None: + return await functions_rpc_interface.delete_function_job( + self._client, + function_job_id=function_job_id, + ) + + async def register_function_job( + self, *, function_job: FunctionJob + ) -> RegisteredFunctionJob: + return await functions_rpc_interface.register_function_job( + self._client, + function_job=function_job, + ) + + async def get_function_input_schema( + self, *, function_id: FunctionID + ) -> FunctionInputSchema: + return await functions_rpc_interface.get_function_input_schema( + self._client, + function_id=function_id, + ) + + async def get_function_output_schema( + self, *, function_id: FunctionID + ) -> FunctionOutputSchema: + return await functions_rpc_interface.get_function_output_schema( + self._client, + function_id=function_id, + ) + + async def find_cached_function_job( + self, *, function_id: FunctionID, inputs: FunctionInputs + ) -> RegisteredFunctionJob | None: + return await functions_rpc_interface.find_cached_function_job( + self._client, function_id=function_id, inputs=inputs + ) + + async def get_function_job_collection( + self, *, function_job_collection_id: FunctionJobCollectionID + ) -> RegisteredFunctionJobCollection: + return await functions_rpc_interface.get_function_job_collection( + self._client, function_job_collection_id=function_job_collection_id + ) + + async def register_function_job_collection( + self, *, function_job_collection: FunctionJobCollection + ) -> RegisteredFunctionJobCollection: + return await functions_rpc_interface.register_function_job_collection( + self._client, function_job_collection=function_job_collection + ) + + async def delete_function_job_collection( + self, *, function_job_collection_id: FunctionJobCollectionID + ) -> None: + return await functions_rpc_interface.delete_function_job_collection( + self._client, function_job_collection_id=function_job_collection_id + ) + def setup(app: FastAPI, rabbitmq_rmp_client: RabbitMQRPCClient): wb_api_rpc_client = WbApiRpcClient(_client=rabbitmq_rmp_client) diff --git a/services/api-server/tests/unit/api_functions/test_api_routers_functions.py b/services/api-server/tests/unit/api_functions/test_api_routers_functions.py new file mode 100644 index 00000000000..d16ae005a8b --- /dev/null +++ b/services/api-server/tests/unit/api_functions/test_api_routers_functions.py @@ -0,0 +1,632 @@ +from unittest.mock import MagicMock +from uuid import uuid4 + +import pytest +from fastapi import FastAPI, HTTPException +from fastapi.testclient import TestClient +from fastapi_pagination import add_pagination +from models_library.api_schemas_webserver.functions_wb_schema import ( + Function, + FunctionJob, + FunctionJobCollection, + JSONFunctionInputSchema, + JSONFunctionOutputSchema, + RegisteredFunction, + RegisteredFunctionJob, + RegisteredFunctionJobCollection, +) +from models_library.rest_pagination import PageMetaInfoLimitOffset +from pydantic import TypeAdapter +from simcore_service_api_server.api.routes.functions_routes import ( + function_job_collections_router, + function_job_router, + function_router, + get_current_user_id, + get_wb_api_rpc_client, +) +from sqlalchemy.ext.asyncio import AsyncEngine + + +@pytest.fixture(name="api_app") +def _api_app() -> FastAPI: + fastapi_app = FastAPI() + fastapi_app.include_router(function_router, prefix="/functions") + fastapi_app.include_router(function_job_router, prefix="/function_jobs") + fastapi_app.include_router( + function_job_collections_router, prefix="/function_job_collections" + ) + add_pagination(fastapi_app) + + # Mock authentication dependency + async def mock_auth_dependency() -> int: + # Mock a valid user ID + return 100 + + fastapi_app.dependency_overrides[get_current_user_id] = mock_auth_dependency + + fake_wb_api_rpc = FakeWbApiRpc() + + async def fake_get_wb_api_rpc_client() -> FakeWbApiRpc: + return fake_wb_api_rpc + + fastapi_app.dependency_overrides[get_wb_api_rpc_client] = fake_get_wb_api_rpc_client + + mock_engine = MagicMock(spec=AsyncEngine) + mock_engine.pool = MagicMock() + mock_engine.pool.checkedin = MagicMock(return_value=[]) + fastapi_app.state.engine = mock_engine + + return fastapi_app + + +class FakeWbApiRpc: + def __init__(self) -> None: + self._functions = {} + self._function_jobs = {} + self._function_job_collections = {} + + async def register_function(self, function: Function) -> RegisteredFunction: + # Mimic returning the same function that was passed and store it for later retrieval + uid = uuid4() + self._functions[uid] = TypeAdapter(RegisteredFunction).validate_python( + { + "uid": str(uid), + "title": function.title, + "function_class": function.function_class, + "project_id": getattr(function, "project_id", None), + "description": function.description, + "input_schema": function.input_schema, + "output_schema": function.output_schema, + "default_inputs": None, + } + ) + return self._functions[uid] + + async def get_function(self, function_id: str) -> RegisteredFunction: + # Mimic retrieval of a function based on function_id and raise 404 if not found + if function_id not in self._functions: + raise HTTPException(status_code=404, detail="Function not found") + return self._functions[function_id] + + async def run_function(self, function_id: str, inputs: dict) -> dict: + # Mimic running a function and returning a success status + if function_id not in self._functions: + raise HTTPException( + status_code=404, + detail=f"Function {function_id} not found in {self._functions}", + ) + return {"status": "success", "function_id": function_id, "inputs": inputs} + + async def list_functions( + self, + pagination_offset: int, + pagination_limit: int, + ) -> tuple[list[RegisteredFunction], PageMetaInfoLimitOffset]: + # Mimic listing all functions + functions_list = list(self._functions.values())[ + pagination_offset : pagination_offset + pagination_limit + ] + total_count = len(self._functions) + page_meta_info = PageMetaInfoLimitOffset( + total=total_count, + limit=pagination_limit, + offset=pagination_offset, + count=len(functions_list), + ) + return functions_list, page_meta_info + + async def delete_function(self, function_id: str) -> None: + # Mimic deleting a function + if function_id in self._functions: + del self._functions[function_id] + else: + raise HTTPException(status_code=404, detail="Function not found") + + async def register_function_job( + self, function_job: FunctionJob + ) -> RegisteredFunctionJob: + # Mimic registering a function job + uid = uuid4() + self._function_jobs[uid] = TypeAdapter(RegisteredFunctionJob).validate_python( + { + "uid": str(uid), + "function_uid": function_job.function_uid, + "title": function_job.title, + "description": function_job.description, + "project_job_id": getattr(function_job, "project_job_id", None), + "inputs": function_job.inputs, + "outputs": function_job.outputs, + "function_class": function_job.function_class, + } + ) + return self._function_jobs[uid] + + async def get_function_job(self, function_job_id: str) -> RegisteredFunctionJob: + # Mimic retrieval of a function job based on function_job_id and raise 404 if not found + if function_job_id not in self._function_jobs: + raise HTTPException(status_code=404, detail="Function job not found") + return self._function_jobs[function_job_id] + + async def list_function_jobs( + self, + pagination_offset: int, + pagination_limit: int, + ) -> tuple[list[RegisteredFunctionJob], PageMetaInfoLimitOffset]: + # Mimic listing all function jobs + function_jobs_list = list(self._function_jobs.values())[ + pagination_offset : pagination_offset + pagination_limit + ] + total_count = len(self._function_jobs) + page_meta_info = PageMetaInfoLimitOffset( + total=total_count, + limit=pagination_limit, + offset=pagination_offset, + count=len(function_jobs_list), + ) + return function_jobs_list, page_meta_info + + async def delete_function_job(self, function_job_id: str) -> None: + # Mimic deleting a function job + if function_job_id in self._function_jobs: + del self._function_jobs[function_job_id] + else: + raise HTTPException(status_code=404, detail="Function job not found") + + async def register_function_job_collection( + self, function_job_collection: FunctionJobCollection + ) -> RegisteredFunctionJobCollection: + # Mimic registering a function job collection + uid = uuid4() + self._function_job_collections[uid] = TypeAdapter( + RegisteredFunctionJobCollection + ).validate_python( + { + "uid": str(uid), + "title": function_job_collection.title, + "description": function_job_collection.description, + "job_ids": function_job_collection.job_ids, + } + ) + return self._function_job_collections[uid] + + async def get_function_job_collection( + self, function_job_collection_id: str + ) -> RegisteredFunctionJobCollection: + # Mimic retrieval of a function job collection based on collection_id and raise 404 if not found + if function_job_collection_id not in self._function_job_collections: + raise HTTPException( + status_code=404, detail="Function job collection not found" + ) + return self._function_job_collections[function_job_collection_id] + + async def list_function_job_collections( + self, + pagination_offset: int, + pagination_limit: int, + ) -> tuple[list[RegisteredFunctionJobCollection], PageMetaInfoLimitOffset]: + # Mimic listing all function job collections + function_job_collections_list = list(self._function_job_collections.values())[ + pagination_offset : pagination_offset + pagination_limit + ] + total_count = len(self._function_job_collections) + page_meta_info = PageMetaInfoLimitOffset( + total=total_count, + limit=pagination_limit, + offset=pagination_offset, + count=len(function_job_collections_list), + ) + return function_job_collections_list, page_meta_info + + async def delete_function_job_collection( + self, function_job_collection_id: str + ) -> None: + # Mimic deleting a function job collection + if function_job_collection_id in self._function_job_collections: + del self._function_job_collections[function_job_collection_id] + else: + raise HTTPException( + status_code=404, detail="Function job collection not found" + ) + + +def test_register_function(api_app) -> None: + client = TestClient(api_app) + sample_function = { + "title": "test_function", + "function_class": "project", + "project_id": str(uuid4()), + "description": "A test function", + "input_schema": JSONFunctionInputSchema().model_dump(), + "output_schema": JSONFunctionOutputSchema().model_dump(), + "default_inputs": None, + } + response = client.post("/functions", json=sample_function) + assert response.status_code == 200 + data = response.json() + assert data["uid"] is not None + assert data["function_class"] == sample_function["function_class"] + assert data["project_id"] == sample_function["project_id"] + assert data["input_schema"] == sample_function["input_schema"] + assert data["output_schema"] == sample_function["output_schema"] + assert data["title"] == sample_function["title"] + assert data["description"] == sample_function["description"] + + +def test_register_function_invalid(api_app: FastAPI) -> None: + client = TestClient(api_app) + invalid_function = { + "title": "test_function", + "function_class": "invalid_class", # Invalid class + "project_id": str(uuid4()), + } + response = client.post("/functions", json=invalid_function) + assert response.status_code == 422 # Unprocessable Entity + assert ( + "Input tag 'invalid_class' found using 'function_class' does no" + in response.json()["detail"][0]["msg"] + ) + + +def test_get_function(api_app: FastAPI) -> None: + client = TestClient(api_app) + project_id = str(uuid4()) + # First, register a sample function so that it exists + sample_function = { + "uid": None, + "title": "example_function", + "function_class": "project", + "project_id": project_id, + "description": "An example function", + "input_schema": JSONFunctionInputSchema().model_dump(), + "output_schema": JSONFunctionOutputSchema().model_dump(), + "default_inputs": None, + } + post_response = client.post("/functions", json=sample_function) + assert post_response.status_code == 200 + data = post_response.json() + function_id = data["uid"] + + expected_function = { + "uid": function_id, + "title": "example_function", + "description": "An example function", + "function_class": "project", + "project_id": project_id, + "input_schema": JSONFunctionInputSchema().model_dump(), + "output_schema": JSONFunctionOutputSchema().model_dump(), + "default_inputs": None, + } + response = client.get(f"/functions/{function_id}") + assert response.status_code == 200 + data = response.json() + # Exclude the 'project_id' field from both expected and actual results before comparing + assert data == expected_function + + +def test_get_function_not_found(api_app: FastAPI) -> None: + client = TestClient(api_app) + non_existent_function_id = str(uuid4()) + response = client.get(f"/functions/{non_existent_function_id}") + assert response.status_code == 404 + assert response.json() == {"detail": "Function not found"} + + +def test_list_functions(api_app: FastAPI) -> None: + client = TestClient(api_app) + # Register a sample function + sample_function = { + "uid": None, + "title": "example_function", + "function_class": "project", + "project_id": str(uuid4()), + "description": "An example function", + "input_schema": JSONFunctionInputSchema().model_dump(), + "output_schema": JSONFunctionOutputSchema().model_dump(), + "default_inputs": None, + } + post_response = client.post("/functions", json=sample_function) + assert post_response.status_code == 200 + + # List functions + response = client.get("/functions", params={"limit": 10, "offset": 0}) + assert response.status_code == 200 + data = response.json()["items"] + assert len(data) > 0 + assert data[0]["title"] == sample_function["title"] + + +def test_get_function_input_schema(api_app: FastAPI) -> None: + client = TestClient(api_app) + project_id = str(uuid4()) + # Register a sample function + sample_function = { + "uid": None, + "title": "example_function", + "function_class": "project", + "project_id": project_id, + "description": "An example function", + "input_schema": JSONFunctionInputSchema( + schema_content={ + "type": "object", + "properties": {"input1": {"type": "integer"}}, + } + ).model_dump(), + "output_schema": JSONFunctionOutputSchema( + schema_content={ + "type": "object", + "properties": {"output1": {"type": "string"}}, + } + ).model_dump(), + "default_inputs": None, + } + post_response = client.post("/functions", json=sample_function) + assert post_response.status_code == 200 + data = post_response.json() + function_id = data["uid"] + + # Get the input schema + # assert f"/functions/{function_id}/input-schema" is None + response = client.get(f"/functions/{function_id}/input_schema") + assert response.status_code == 200 + data = response.json() + assert data["schema_content"] == sample_function["input_schema"]["schema_content"] + + +def test_get_function_output_schema(api_app: FastAPI) -> None: + client = TestClient(api_app) + project_id = str(uuid4()) + # Register a sample function + sample_function = { + "uid": None, + "title": "example_function", + "function_class": "project", + "project_id": project_id, + "description": "An example function", + "input_schema": JSONFunctionInputSchema().model_dump(), + "output_schema": JSONFunctionOutputSchema( + schema_content={ + "type": "object", + "properties": {"output1": {"type": "string"}}, + } + ).model_dump(), + "default_inputs": None, + } + post_response = client.post("/functions", json=sample_function) + assert post_response.status_code == 200 + data = post_response.json() + function_id = data["uid"] + + # Get the output schema + response = client.get(f"/functions/{function_id}/output_schema") + assert response.status_code == 200 + data = response.json() + assert data["schema_content"] == sample_function["output_schema"]["schema_content"] + + +def test_validate_function_inputs(api_app: FastAPI) -> None: + client = TestClient(api_app) + project_id = str(uuid4()) + # Register a sample function + sample_function = { + "uid": None, + "title": "example_function", + "function_class": "project", + "project_id": project_id, + "description": "An example function", + "input_schema": JSONFunctionInputSchema( + schema_content={ + "type": "object", + "properties": {"input1": {"type": "integer"}}, + } + ).model_dump(), + "output_schema": JSONFunctionOutputSchema( + schema_content={ + "type": "object", + "properties": {"output1": {"type": "string"}}, + } + ).model_dump(), + "default_inputs": None, + } + post_response = client.post("/functions", json=sample_function) + assert post_response.status_code == 200 + data = post_response.json() + function_id = data["uid"] + + # Validate inputs + validate_payload = {"input1": 10} + response = client.post( + f"/functions/{function_id}:validate_inputs", json=validate_payload + ) + assert response.status_code == 200 + data = response.json() + assert data == [True, "Inputs are valid"] + + +def test_delete_function(api_app: FastAPI) -> None: + client = TestClient(api_app) + project_id = str(uuid4()) + # Register a sample function + sample_function = { + "uid": None, + "title": "example_function", + "function_class": "project", + "project_id": project_id, + "description": "An example function", + "input_schema": JSONFunctionInputSchema().model_dump(), + "output_schema": JSONFunctionOutputSchema().model_dump(), + "default_inputs": None, + } + post_response = client.post("/functions", json=sample_function) + assert post_response.status_code == 200 + data = post_response.json() + function_id = data["uid"] + + # Delete the function + response = client.delete(f"/functions/{function_id}") + assert response.status_code == 200 + + +def test_register_function_job(api_app: FastAPI) -> None: + """Test the register_function_job endpoint.""" + + client = TestClient(api_app) + mock_function_job = { + "function_uid": str(uuid4()), + "title": "Test Function Job", + "description": "A test function job", + "inputs": {"key": "value"}, + "outputs": None, + "project_job_id": str(uuid4()), + "function_class": "project", + } + + # Act + response = client.post("/function_jobs", json=mock_function_job) + + # Assert + assert response.status_code == 200 + response_data = response.json() + assert response_data["uid"] is not None + response_data.pop("uid", None) # Remove the uid field + mock_function_job.pop("uid", None) # Remove the uid field + assert response_data == mock_function_job + + +def test_get_function_job(api_app: FastAPI) -> None: + """Test the get_function_job endpoint.""" + + client = TestClient(api_app) + mock_function_job = { + "uid": None, + "function_uid": str(uuid4()), + "title": "Test Function Job", + "description": "A test function job", + "inputs": {"key": "value"}, + "outputs": None, + "project_job_id": str(uuid4()), + "function_class": "project", + } + + # First, register a function job + post_response = client.post("/function_jobs", json=mock_function_job) + assert post_response.status_code == 200 + data = post_response.json() + function_job_id = data["uid"] + + # Now, get the function job + response = client.get(f"/function_jobs/{function_job_id}") + assert response.status_code == 200 + data = response.json() + assert data["uid"] == function_job_id + assert data["title"] == mock_function_job["title"] + assert data["description"] == mock_function_job["description"] + assert data["inputs"] == mock_function_job["inputs"] + assert data["outputs"] == mock_function_job["outputs"] + + +def test_list_function_jobs(api_app: FastAPI) -> None: + """Test the list_function_jobs endpoint.""" + + client = TestClient(api_app) + mock_function_job = { + "uid": None, + "function_uid": str(uuid4()), + "title": "Test Function Job", + "description": "A test function job", + "inputs": {"key": "value"}, + "outputs": None, + "project_job_id": str(uuid4()), + "function_class": "project", + } + + # First, register a function job + post_response = client.post("/function_jobs", json=mock_function_job) + assert post_response.status_code == 200 + + # Now, list function jobs + response = client.get("/function_jobs") + assert response.status_code == 200 + data = response.json()["items"] + assert len(data) > 0 + assert data[0]["title"] == mock_function_job["title"] + + +def test_delete_function_job(api_app: FastAPI) -> None: + """Test the delete_function_job endpoint.""" + + client = TestClient(api_app) + mock_function_job = { + "uid": None, + "function_uid": str(uuid4()), + "title": "Test Function Job", + "description": "A test function job", + "inputs": {"key": "value"}, + "outputs": None, + "project_job_id": str(uuid4()), + "function_class": "project", + } + + # First, register a function job + post_response = client.post("/function_jobs", json=mock_function_job) + assert post_response.status_code == 200 + data = post_response.json() + function_job_id = data["uid"] + + # Now, delete the function job + response = client.delete(f"/function_jobs/{function_job_id}") + assert response.status_code == 200 + + +def test_register_function_job_collection(api_app: FastAPI) -> None: + # Arrange + client = TestClient(api_app) + + mock_function_job_collection = { + "uid": None, + "title": "Test Collection", + "description": "A test function job collection", + "job_ids": [str(uuid4()), str(uuid4())], + } + + # Act + response = client.post( + "/function_job_collections", json=mock_function_job_collection + ) + + # Assert + assert response.status_code == 200 + response_data = response.json() + assert response_data["uid"] is not None + response_data.pop("uid", None) # Remove the uid field + mock_function_job_collection.pop("uid", None) # Remove the uid field + assert response_data == mock_function_job_collection + + +def test_get_function_job_collection(api_app: FastAPI) -> None: + # Arrange + client = TestClient(api_app) + mock_function_job_collection = { + "uid": None, + "title": "Test Collection", + "description": "A test function job collection", + "job_ids": [str(uuid4()), str(uuid4())], + } + + # First, register a function job collection + post_response = client.post( + "/function_job_collections", json=mock_function_job_collection + ) + assert post_response.status_code == 200 + data = post_response.json() + collection_id = data["uid"] + + # Act + response = client.get(f"/function_job_collections/{collection_id}") + + # Assert + assert response.status_code == 200 + data = response.json() + assert data["uid"] == collection_id + assert data["title"] == mock_function_job_collection["title"] + assert data["description"] == mock_function_job_collection["description"] + assert data["job_ids"] == mock_function_job_collection["job_ids"] diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller_rpc.py b/services/web/server/src/simcore_service_webserver/functions/_controller_rpc.py deleted file mode 100644 index 483fcdc26e3..00000000000 --- a/services/web/server/src/simcore_service_webserver/functions/_controller_rpc.py +++ /dev/null @@ -1,21 +0,0 @@ -from aiohttp import web -from models_library.api_schemas_webserver import WEBSERVER_RPC_NAMESPACE -from servicelib.rabbitmq import RPCRouter - -from ..rabbitmq import get_rabbitmq_rpc_server - -# this is the rpc interface exposed to the api-server -# this interface should call the service layer - -router = RPCRouter() - - -@router.expose() -async def ping(app: web.Application) -> str: - assert app - return "pong from webserver" - - -async def register_rpc_routes_on_startup(app: web.Application): - rpc_server = get_rabbitmq_rpc_server(app) - await rpc_server.register_router(router, WEBSERVER_RPC_NAMESPACE, app) diff --git a/services/web/server/src/simcore_service_webserver/functions/_functions_controller_rpc.py b/services/web/server/src/simcore_service_webserver/functions/_functions_controller_rpc.py new file mode 100644 index 00000000000..6cf8dd878ef --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/functions/_functions_controller_rpc.py @@ -0,0 +1,426 @@ +from aiohttp import web +from functions_rpc.test_functions_controller_rpc import FunctionJobCollection +from models_library.api_schemas_webserver import WEBSERVER_RPC_NAMESPACE +from models_library.api_schemas_webserver.functions_wb_schema import ( + Function, + FunctionClass, + FunctionClassSpecificData, + FunctionDB, + FunctionID, + FunctionIDNotFoundError, + FunctionInputs, + FunctionInputSchema, + FunctionJob, + FunctionJobClassSpecificData, + FunctionJobCollectionIDNotFoundError, + FunctionJobDB, + FunctionJobID, + FunctionJobIDNotFoundError, + FunctionOutputSchema, + RegisteredFunction, + RegisteredFunctionJob, + RegisteredFunctionJobCollection, + RegisteredProjectFunction, + RegisteredProjectFunctionJob, + RegisteredSolverFunction, + RegisteredSolverFunctionJob, + UnsupportedFunctionClassError, + UnsupportedFunctionJobClassError, +) +from models_library.rest_pagination import PageMetaInfoLimitOffset +from servicelib.rabbitmq import RPCRouter + +from ..rabbitmq import get_rabbitmq_rpc_server +from . import _functions_repository +from ._functions_repository import RegisteredFunctionDB, RegisteredFunctionJobDB + +router = RPCRouter() + +# pylint: disable=no-else-return + + +@router.expose(reraise_if_error_type=(UnsupportedFunctionClassError,)) +async def register_function( + app: web.Application, *, function: Function +) -> RegisteredFunction: + assert app + + encoded_function = _encode_function(function) + saved_function = await _functions_repository.create_function( + app=app, + title=encoded_function.title, + function_class=encoded_function.function_class, + description=encoded_function.description, + input_schema=encoded_function.input_schema, + output_schema=encoded_function.output_schema, + default_inputs=encoded_function.default_inputs, + class_specific_data=encoded_function.class_specific_data, + ) + return _decode_function(saved_function) + + +@router.expose(reraise_if_error_type=(UnsupportedFunctionJobClassError,)) +async def register_function_job( + app: web.Application, *, function_job: FunctionJob +) -> RegisteredFunctionJob: + assert app + encoded_function_job = _encode_functionjob(function_job) + created_function_job_db = await _functions_repository.create_function_job( + app=app, + function_class=encoded_function_job.function_class, + title=encoded_function_job.title, + description=encoded_function_job.description, + function_uid=encoded_function_job.function_uuid, + inputs=encoded_function_job.inputs, + outputs=encoded_function_job.outputs, + class_specific_data=encoded_function_job.class_specific_data, + ) + return _decode_functionjob(created_function_job_db) + + +@router.expose(reraise_if_error_type=()) +async def register_function_job_collection( + app: web.Application, *, function_job_collection: FunctionJobCollection +) -> RegisteredFunctionJobCollection: + assert app + registered_function_job_collection, registered_job_ids = ( + await _functions_repository.create_function_job_collection( + app=app, + title=function_job_collection.title, + description=function_job_collection.description, + job_ids=function_job_collection.job_ids, + ) + ) + return RegisteredFunctionJobCollection( + uid=registered_function_job_collection.uuid, + title=registered_function_job_collection.title, + description=registered_function_job_collection.description, + job_ids=registered_job_ids, + ) + + +@router.expose(reraise_if_error_type=(FunctionIDNotFoundError,)) +async def get_function( + app: web.Application, *, function_id: FunctionID +) -> RegisteredFunction: + assert app + returned_function = await _functions_repository.get_function( + app=app, + function_id=function_id, + ) + return _decode_function( + returned_function, + ) + + +@router.expose(reraise_if_error_type=(FunctionJobIDNotFoundError,)) +async def get_function_job( + app: web.Application, *, function_job_id: FunctionJobID +) -> RegisteredFunctionJob: + assert app + returned_function_job = await _functions_repository.get_function_job( + app=app, + function_job_id=function_job_id, + ) + assert returned_function_job is not None + + return _decode_functionjob(returned_function_job) + + +@router.expose(reraise_if_error_type=(FunctionJobCollectionIDNotFoundError,)) +async def get_function_job_collection( + app: web.Application, *, function_job_collection_id: FunctionJobID +) -> RegisteredFunctionJobCollection: + assert app + returned_function_job_collection, returned_job_ids = ( + await _functions_repository.get_function_job_collection( + app=app, + function_job_collection_id=function_job_collection_id, + ) + ) + return RegisteredFunctionJobCollection( + uid=returned_function_job_collection.uuid, + title=returned_function_job_collection.title, + description=returned_function_job_collection.description, + job_ids=returned_job_ids, + ) + + +@router.expose() +async def list_functions( + app: web.Application, + pagination_limit: int, + pagination_offset: int, +) -> tuple[list[RegisteredFunction], PageMetaInfoLimitOffset]: + assert app + returned_functions, page = await _functions_repository.list_functions( + app=app, + pagination_limit=pagination_limit, + pagination_offset=pagination_offset, + ) + return [ + _decode_function(returned_function) for returned_function in returned_functions + ], page + + +@router.expose() +async def list_function_jobs( + app: web.Application, + pagination_limit: int, + pagination_offset: int, +) -> tuple[list[RegisteredFunctionJob], PageMetaInfoLimitOffset]: + assert app + returned_function_jobs, page = await _functions_repository.list_function_jobs( + app=app, + pagination_limit=pagination_limit, + pagination_offset=pagination_offset, + ) + return [ + _decode_functionjob(returned_function_job) + for returned_function_job in returned_function_jobs + ], page + + +@router.expose() +async def list_function_job_collections( + app: web.Application, + pagination_limit: int, + pagination_offset: int, +) -> tuple[list[RegisteredFunctionJobCollection], PageMetaInfoLimitOffset]: + assert app + returned_function_job_collections, page = ( + await _functions_repository.list_function_job_collections( + app=app, + pagination_limit=pagination_limit, + pagination_offset=pagination_offset, + ) + ) + return [ + RegisteredFunctionJobCollection( + uid=function_job_collection.uuid, + title=function_job_collection.title, + description=function_job_collection.description, + job_ids=job_ids, + ) + for function_job_collection, job_ids in returned_function_job_collections + ], page + + +@router.expose(reraise_if_error_type=(FunctionIDNotFoundError,)) +async def delete_function(app: web.Application, *, function_id: FunctionID) -> None: + assert app + await _functions_repository.delete_function( + app=app, + function_id=function_id, + ) + + +@router.expose(reraise_if_error_type=(FunctionJobIDNotFoundError,)) +async def delete_function_job( + app: web.Application, *, function_job_id: FunctionJobID +) -> None: + assert app + await _functions_repository.delete_function_job( + app=app, + function_job_id=function_job_id, + ) + + +@router.expose(reraise_if_error_type=(FunctionJobCollectionIDNotFoundError,)) +async def delete_function_job_collection( + app: web.Application, *, function_job_collection_id: FunctionJobID +) -> None: + assert app + await _functions_repository.delete_function_job_collection( + app=app, + function_job_collection_id=function_job_collection_id, + ) + + +@router.expose() +async def find_cached_function_job( + app: web.Application, *, function_id: FunctionID, inputs: FunctionInputs +) -> FunctionJob | None: + assert app + returned_function_job = await _functions_repository.find_cached_function_job( + app=app, function_id=function_id, inputs=inputs + ) + if returned_function_job is None: + return None + + if returned_function_job.function_class == FunctionClass.project: + return RegisteredProjectFunctionJob( + uid=returned_function_job.uuid, + title=returned_function_job.title, + description="", + function_uid=returned_function_job.function_uuid, + inputs=returned_function_job.inputs, + outputs=None, + project_job_id=returned_function_job.class_specific_data["project_job_id"], + ) + elif returned_function_job.function_class == FunctionClass.solver: # noqa: RET505 + return RegisteredSolverFunctionJob( + uid=returned_function_job.uuid, + title=returned_function_job.title, + description="", + function_uid=returned_function_job.function_uuid, + inputs=returned_function_job.inputs, + outputs=None, + solver_job_id=returned_function_job.class_specific_data["solver_job_id"], + ) + else: + raise UnsupportedFunctionJobClassError( + function_job_class=returned_function_job.function_class + ) + + +@router.expose(reraise_if_error_type=(FunctionIDNotFoundError,)) +async def get_function_input_schema( + app: web.Application, *, function_id: FunctionID +) -> FunctionInputSchema: + assert app + returned_function = await _functions_repository.get_function( + app=app, + function_id=function_id, + ) + return _decode_function(returned_function).input_schema + + +@router.expose(reraise_if_error_type=(FunctionIDNotFoundError,)) +async def get_function_output_schema( + app: web.Application, *, function_id: FunctionID +) -> FunctionOutputSchema: + assert app + returned_function = await _functions_repository.get_function( + app=app, + function_id=function_id, + ) + return _decode_function(returned_function).output_schema + + +def _decode_function( + function: RegisteredFunctionDB, +) -> RegisteredFunction: + if function.function_class == "project": + return RegisteredProjectFunction( + uid=function.uuid, + title=function.title, + description=function.description, + input_schema=function.input_schema, + output_schema=function.output_schema, + project_id=function.class_specific_data["project_id"], + default_inputs=function.default_inputs, + ) + elif function.function_class == "solver": # noqa: RET505 + return RegisteredSolverFunction( + uid=function.uuid, + title=function.title, + description=function.description, + input_schema=function.input_schema, + output_schema=function.output_schema, + solver_key=function.class_specific_data["solver_key"], + solver_version=function.class_specific_data["solver_version"], + default_inputs=function.default_inputs, + ) + else: + raise UnsupportedFunctionClassError(function_class=function.function_class) + + +def _encode_function( + function: Function, +) -> FunctionDB: + if function.function_class == FunctionClass.project: + class_specific_data = FunctionClassSpecificData( + {"project_id": str(function.project_id)} + ) + elif function.function_class == FunctionClass.solver: + class_specific_data = FunctionClassSpecificData( + { + "solver_key": str(function.solver_key), + "solver_version": str(function.solver_version), + } + ) + else: + raise UnsupportedFunctionClassError(function_class=function.function_class) + + return FunctionDB( + title=function.title, + description=function.description, + input_schema=function.input_schema, + output_schema=function.output_schema, + default_inputs=function.default_inputs, + class_specific_data=class_specific_data, + function_class=function.function_class, + ) + + +def _encode_functionjob( + functionjob: FunctionJob, +) -> FunctionJobDB: + + if functionjob.function_class == FunctionClass.project: + return FunctionJobDB( + title=functionjob.title, + function_uuid=functionjob.function_uid, + inputs=functionjob.inputs, + outputs=functionjob.outputs, + class_specific_data=FunctionJobClassSpecificData( + { + "project_job_id": str(functionjob.project_job_id), + } + ), + function_class=functionjob.function_class, + ) + + if functionjob.function_class == FunctionClass.solver: + return FunctionJobDB( + title=functionjob.title, + function_uuid=functionjob.function_uid, + inputs=functionjob.inputs, + outputs=functionjob.outputs, + class_specific_data=FunctionJobClassSpecificData( + { + "solver_job_id": str(functionjob.solver_job_id), + } + ), + function_class=functionjob.function_class, + ) + + raise UnsupportedFunctionJobClassError( + function_job_class=functionjob.function_class + ) + + +def _decode_functionjob( + functionjob_db: RegisteredFunctionJobDB, +) -> RegisteredFunctionJob: + if functionjob_db.function_class == FunctionClass.project: + return RegisteredProjectFunctionJob( + uid=functionjob_db.uuid, + title=functionjob_db.title, + description="", + function_uid=functionjob_db.function_uuid, + inputs=functionjob_db.inputs, + outputs=functionjob_db.outputs, + project_job_id=functionjob_db.class_specific_data["project_job_id"], + ) + + if functionjob_db.function_class == FunctionClass.solver: + return RegisteredSolverFunctionJob( + uid=functionjob_db.uuid, + title=functionjob_db.title, + description="", + function_uid=functionjob_db.function_uuid, + inputs=functionjob_db.inputs, + outputs=functionjob_db.outputs, + solver_job_id=functionjob_db.class_specific_data["solver_job_id"], + ) + + raise UnsupportedFunctionJobClassError( + function_job_class=functionjob_db.function_class + ) + + +async def register_rpc_routes_on_startup(app: web.Application): + rpc_server = get_rabbitmq_rpc_server(app) + await rpc_server.register_router(router, WEBSERVER_RPC_NAMESPACE, app) diff --git a/services/web/server/src/simcore_service_webserver/functions/_functions_repository.py b/services/web/server/src/simcore_service_webserver/functions/_functions_repository.py new file mode 100644 index 00000000000..b936d0bbb4e --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/functions/_functions_repository.py @@ -0,0 +1,482 @@ +import json + +from aiohttp import web +from models_library.api_schemas_webserver.functions_wb_schema import ( + FunctionClass, + FunctionID, + FunctionIDNotFoundError, + FunctionInputs, + FunctionInputSchema, + FunctionJobClassSpecificData, + FunctionJobCollectionIDNotFoundError, + FunctionJobID, + FunctionJobIDNotFoundError, + FunctionOutputs, + FunctionOutputSchema, + RegisteredFunctionDB, + RegisteredFunctionJobCollectionDB, + RegisteredFunctionJobDB, +) +from models_library.rest_pagination import PageMetaInfoLimitOffset +from simcore_postgres_database.models.funcapi_function_job_collections_table import ( + function_job_collections_table, +) +from simcore_postgres_database.models.funcapi_function_job_collections_to_function_jobs_table import ( + function_job_collections_to_function_jobs_table, +) +from simcore_postgres_database.models.funcapi_function_jobs_table import ( + function_jobs_table, +) +from simcore_postgres_database.models.funcapi_functions_table import functions_table +from simcore_postgres_database.utils_repos import ( + get_columns_from_db_model, + transaction_context, +) +from sqlalchemy import Text, cast +from sqlalchemy.ext.asyncio import AsyncConnection +from sqlalchemy.sql import func + +from ..db.plugin import get_asyncpg_engine + +_FUNCTIONS_TABLE_COLS = get_columns_from_db_model(functions_table, RegisteredFunctionDB) +_FUNCTION_JOBS_TABLE_COLS = get_columns_from_db_model( + function_jobs_table, RegisteredFunctionJobDB +) +_FUNCTION_JOB_COLLECTIONS_TABLE_COLS = get_columns_from_db_model( + function_job_collections_table, RegisteredFunctionJobCollectionDB +) + + +async def create_function( + app: web.Application, + connection: AsyncConnection | None = None, + *, + function_class: FunctionClass, + class_specific_data: dict, + title: str, + description: str, + input_schema: FunctionInputSchema, + output_schema: FunctionOutputSchema, + default_inputs: FunctionInputs, +) -> RegisteredFunctionDB: + + async with transaction_context(get_asyncpg_engine(app), connection) as conn: + result = await conn.stream( + functions_table.insert() + .values( + title=title, + description=description, + input_schema=(input_schema.model_dump()), + output_schema=(output_schema.model_dump()), + function_class=function_class, + class_specific_data=class_specific_data, + default_inputs=default_inputs, + ) + .returning(*_FUNCTIONS_TABLE_COLS) + ) + row = await result.first() + + assert row is not None, ( + "No row was returned from the database after creating function." + f" Function: {title}" + ) # nosec + + return RegisteredFunctionDB.model_validate(dict(row)) + + +async def create_function_job( + app: web.Application, + connection: AsyncConnection | None = None, + *, + function_class: FunctionClass, + function_uid: FunctionID, + title: str, + description: str, + inputs: FunctionInputs, + outputs: FunctionOutputs, + class_specific_data: FunctionJobClassSpecificData, +) -> RegisteredFunctionJobDB: + + async with transaction_context(get_asyncpg_engine(app), connection) as conn: + result = await conn.stream( + function_jobs_table.insert() + .values( + function_uuid=function_uid, + inputs=inputs, + outputs=outputs, + function_class=function_class, + class_specific_data=class_specific_data, + title=title, + description=description, + status="created", + ) + .returning(*_FUNCTION_JOBS_TABLE_COLS) + ) + row = await result.first() + + assert row is not None, ( + "No row was returned from the database after creating function job." + f" Function job: {title}" + ) # nosec + + return RegisteredFunctionJobDB.model_validate(dict(row)) + + +async def create_function_job_collection( + app: web.Application, + connection: AsyncConnection | None = None, + *, + title: str, + description: str, + job_ids: list[FunctionJobID], +) -> tuple[RegisteredFunctionJobCollectionDB, list[FunctionJobID]]: + async with transaction_context(get_asyncpg_engine(app), connection) as conn: + result = await conn.stream( + function_job_collections_table.insert() + .values( + title=title, + description=description, + ) + .returning(*_FUNCTION_JOB_COLLECTIONS_TABLE_COLS) + ) + row = await result.first() + + assert row is not None, ( + "No row was returned from the database after creating function job collection." + f" Function job collection: {title}" + ) # nosec + + function_job_collection_db = RegisteredFunctionJobCollectionDB.model_validate( + dict(row) + ) + job_collection_entries = [] + for job_id in job_ids: + result = await conn.stream( + function_job_collections_to_function_jobs_table.insert() + .values( + function_job_collection_uuid=function_job_collection_db.uuid, + function_job_uuid=job_id, + ) + .returning( + function_job_collections_to_function_jobs_table.c.function_job_collection_uuid, + function_job_collections_to_function_jobs_table.c.function_job_uuid, + ) + ) + entry = await result.first() + assert entry is not None, ( + f"No row was returned from the database after creating function job collection entry {title}." + f" Job ID: {job_id}" + ) # nosec + job_collection_entries.append(dict(entry)) + + return function_job_collection_db, [ + dict(entry)["function_job_uuid"] for entry in job_collection_entries + ] + + +async def get_function( + app: web.Application, + connection: AsyncConnection | None = None, + *, + function_id: FunctionID, +) -> RegisteredFunctionDB: + + async with transaction_context(get_asyncpg_engine(app), connection) as conn: + result = await conn.stream( + functions_table.select().where(functions_table.c.uuid == function_id) + ) + row = await result.first() + + if row is None: + raise FunctionIDNotFoundError(function_id=function_id) + return RegisteredFunctionDB.model_validate(dict(row)) + + +async def list_functions( + app: web.Application, + connection: AsyncConnection | None = None, + *, + pagination_limit: int, + pagination_offset: int, +) -> tuple[list[RegisteredFunctionDB], PageMetaInfoLimitOffset]: + + async with transaction_context(get_asyncpg_engine(app), connection) as conn: + total_count_result = await conn.scalar( + func.count().select().select_from(functions_table) + ) + result = await conn.stream( + functions_table.select().offset(pagination_offset).limit(pagination_limit) + ) + rows = await result.all() + if rows is None: + return [], PageMetaInfoLimitOffset( + total=0, offset=pagination_offset, limit=pagination_limit, count=0 + ) + + return [ + RegisteredFunctionDB.model_validate(dict(row)) for row in rows + ], PageMetaInfoLimitOffset( + total=total_count_result, + offset=pagination_offset, + limit=pagination_limit, + count=len(rows), + ) + + +async def list_function_jobs( + app: web.Application, + connection: AsyncConnection | None = None, + *, + pagination_limit: int, + pagination_offset: int, +) -> tuple[list[RegisteredFunctionJobDB], PageMetaInfoLimitOffset]: + + async with transaction_context(get_asyncpg_engine(app), connection) as conn: + total_count_result = await conn.scalar( + func.count().select().select_from(function_jobs_table) + ) + result = await conn.stream( + function_jobs_table.select() + .offset(pagination_offset) + .limit(pagination_limit) + ) + rows = await result.all() + if rows is None: + return [], PageMetaInfoLimitOffset( + total=0, offset=pagination_offset, limit=pagination_limit, count=0 + ) + + return [ + RegisteredFunctionJobDB.model_validate(dict(row)) for row in rows + ], PageMetaInfoLimitOffset( + total=total_count_result, + offset=pagination_offset, + limit=pagination_limit, + count=len(rows), + ) + + +async def list_function_job_collections( + app: web.Application, + connection: AsyncConnection | None = None, + *, + pagination_limit: int, + pagination_offset: int, +) -> tuple[ + list[tuple[RegisteredFunctionJobCollectionDB, list[FunctionJobID]]], + PageMetaInfoLimitOffset, +]: + """ + Returns a list of function job collections and their associated job ids. + """ + async with transaction_context(get_asyncpg_engine(app), connection) as conn: + total_count_result = await conn.scalar( + func.count().select().select_from(function_job_collections_table) + ) + result = await conn.stream( + function_job_collections_table.select() + .offset(pagination_offset) + .limit(pagination_limit) + ) + rows = await result.all() + if rows is None: + return [], PageMetaInfoLimitOffset( + total=0, offset=pagination_offset, limit=pagination_limit, count=0 + ) + + collections = [] + for row in rows: + collection = RegisteredFunctionJobCollectionDB.model_validate(dict(row)) + job_result = await conn.stream( + function_job_collections_to_function_jobs_table.select().where( + function_job_collections_to_function_jobs_table.c.function_job_collection_uuid + == row["uuid"] + ) + ) + job_rows = await job_result.all() + job_ids = ( + [job_row["function_job_uuid"] for job_row in job_rows] + if job_rows + else [] + ) + collections.append((collection, job_ids)) + return collections, PageMetaInfoLimitOffset( + total=total_count_result, + offset=pagination_offset, + limit=pagination_limit, + count=len(rows), + ) + + +async def delete_function( + app: web.Application, + connection: AsyncConnection | None = None, + *, + function_id: FunctionID, +) -> None: + + async with transaction_context(get_asyncpg_engine(app), connection) as conn: + # Check if the function exists + result = await conn.stream( + functions_table.select().where(functions_table.c.uuid == function_id) + ) + row = await result.first() + + if row is None: + raise FunctionIDNotFoundError(function_id=function_id) + + # Proceed with deletion + await conn.execute( + functions_table.delete().where(functions_table.c.uuid == function_id) + ) + + +async def get_function_job( + app: web.Application, + connection: AsyncConnection | None = None, + *, + function_job_id: FunctionID, +) -> RegisteredFunctionJobDB: + + async with transaction_context(get_asyncpg_engine(app), connection) as conn: + result = await conn.stream( + function_jobs_table.select().where( + function_jobs_table.c.uuid == function_job_id + ) + ) + row = await result.first() + + if row is None: + raise FunctionJobIDNotFoundError(function_job_id=function_job_id) + + return RegisteredFunctionJobDB.model_validate(dict(row)) + + +async def delete_function_job( + app: web.Application, + connection: AsyncConnection | None = None, + *, + function_job_id: FunctionID, +) -> None: + async with transaction_context(get_asyncpg_engine(app), connection) as conn: + # Check if the function job exists + result = await conn.stream( + function_jobs_table.select().where( + function_jobs_table.c.uuid == function_job_id + ) + ) + row = await result.first() + if row is None: + raise FunctionJobIDNotFoundError(function_job_id=function_job_id) + + # Proceed with deletion + await conn.execute( + function_jobs_table.delete().where( + function_jobs_table.c.uuid == function_job_id + ) + ) + + +async def find_cached_function_job( + app: web.Application, + connection: AsyncConnection | None = None, + *, + function_id: FunctionID, + inputs: FunctionInputs, +) -> RegisteredFunctionJobDB | None: + + async with transaction_context(get_asyncpg_engine(app), connection) as conn: + result = await conn.stream( + function_jobs_table.select().where( + function_jobs_table.c.function_uuid == function_id, + cast(function_jobs_table.c.inputs, Text) == json.dumps(inputs), + ), + ) + + rows = await result.all() + + if rows is None or len(rows) == 0: + return None + + assert len(rows) == 1, ( + "More than one function job found with the same function id and inputs." + f" Function id: {function_id}, Inputs: {inputs}" + ) # nosec + + row = rows[0] + + job = RegisteredFunctionJobDB.model_validate(dict(row)) + if job.inputs == inputs: + return job + + return None + + +async def get_function_job_collection( + app: web.Application, + connection: AsyncConnection | None = None, + *, + function_job_collection_id: FunctionID, +) -> tuple[RegisteredFunctionJobCollectionDB, list[FunctionJobID]]: + + async with transaction_context(get_asyncpg_engine(app), connection) as conn: + result = await conn.stream( + function_job_collections_table.select().where( + function_job_collections_table.c.uuid == function_job_collection_id + ) + ) + row = await result.first() + + if row is None: + raise FunctionJobCollectionIDNotFoundError( + function_job_collection_id=function_job_collection_id + ) + + # Retrieve associated job ids from the join table + job_result = await conn.stream( + function_job_collections_to_function_jobs_table.select().where( + function_job_collections_to_function_jobs_table.c.function_job_collection_uuid + == row["uuid"] + ) + ) + job_rows = await job_result.all() + + job_ids = ( + [job_row["function_job_uuid"] for job_row in job_rows] if job_rows else [] + ) + + job_collection = RegisteredFunctionJobCollectionDB.model_validate(dict(row)) + return job_collection, job_ids + + +async def delete_function_job_collection( + app: web.Application, + connection: AsyncConnection | None = None, + *, + function_job_collection_id: FunctionID, +) -> None: + + async with transaction_context(get_asyncpg_engine(app), connection) as conn: + # Check if the function job collection exists + result = await conn.stream( + function_job_collections_table.select().where( + function_job_collections_table.c.uuid == function_job_collection_id + ) + ) + row = await result.first() + if row is None: + raise FunctionJobCollectionIDNotFoundError( + function_job_collection_id=function_job_collection_id + ) + # Proceed with deletion + await conn.execute( + function_job_collections_table.delete().where( + function_job_collections_table.c.uuid == function_job_collection_id + ) + ) + await conn.execute( + function_job_collections_to_function_jobs_table.delete().where( + function_job_collections_to_function_jobs_table.c.function_job_collection_uuid + == function_job_collection_id + ) + ) diff --git a/services/web/server/src/simcore_service_webserver/functions/_repo.py b/services/web/server/src/simcore_service_webserver/functions/_repo.py deleted file mode 100644 index baf76a9ee8d..00000000000 --- a/services/web/server/src/simcore_service_webserver/functions/_repo.py +++ /dev/null @@ -1,2 +0,0 @@ -# the repository layer. Calls directly to the db (function table) should be done here. -# see e.g. licenses/_licensed_resources_repository.py file for an example diff --git a/services/web/server/src/simcore_service_webserver/functions/_service.py b/services/web/server/src/simcore_service_webserver/functions/_service.py deleted file mode 100644 index 2c967d7c841..00000000000 --- a/services/web/server/src/simcore_service_webserver/functions/_service.py +++ /dev/null @@ -1,21 +0,0 @@ -# This is where calls to the business logic is done. -# calls to the `projects` interface should be done here. -# calls to _repo.py should also be done here - -from aiohttp import web -from models_library.users import UserID - -from ..projects import _projects_service -from ..projects.models import ProjectDict - - -# example function -async def get_project_from_function( - app: web.Application, - function_uuid: str, - user_id: UserID, -) -> ProjectDict: - - return await _projects_service.get_project_for_user( - app=app, project_uuid=function_uuid, user_id=user_id - ) diff --git a/services/web/server/src/simcore_service_webserver/functions/plugin.py b/services/web/server/src/simcore_service_webserver/functions/plugin.py index 364436f4898..8d18e55a034 100644 --- a/services/web/server/src/simcore_service_webserver/functions/plugin.py +++ b/services/web/server/src/simcore_service_webserver/functions/plugin.py @@ -3,7 +3,7 @@ from aiohttp import web from servicelib.aiohttp.application_setup import ModuleCategory, app_module_setup -from . import _controller_rpc +from . import _functions_controller_rpc _logger = logging.getLogger(__name__) @@ -15,4 +15,4 @@ logger=_logger, ) def setup_functions(app: web.Application): - app.on_startup.append(_controller_rpc.register_rpc_routes_on_startup) + app.on_startup.append(_functions_controller_rpc.register_rpc_routes_on_startup) diff --git a/services/web/server/tests/unit/with_dbs/functions_rpc/test_functions_controller_rpc.py b/services/web/server/tests/unit/with_dbs/functions_rpc/test_functions_controller_rpc.py new file mode 100644 index 00000000000..96db2abb826 --- /dev/null +++ b/services/web/server/tests/unit/with_dbs/functions_rpc/test_functions_controller_rpc.py @@ -0,0 +1,466 @@ +# pylint: disable=redefined-outer-name +from uuid import uuid4 + +import pytest +import simcore_service_webserver.functions._functions_controller_rpc as functions_rpc +from models_library.api_schemas_webserver.functions_wb_schema import ( + Function, + FunctionIDNotFoundError, + FunctionJobCollection, + FunctionJobIDNotFoundError, + JSONFunctionInputSchema, + JSONFunctionOutputSchema, + ProjectFunction, + ProjectFunctionJob, +) + + +@pytest.fixture +def mock_function() -> Function: + return ProjectFunction( + title="Test Function", + description="A test function", + input_schema=JSONFunctionInputSchema( + schema_content={ + "type": "object", + "properties": {"input1": {"type": "string"}}, + } + ), + output_schema=JSONFunctionOutputSchema( + schema_content={ + "type": "object", + "properties": {"output1": {"type": "string"}}, + } + ), + project_id=uuid4(), + default_inputs=None, + ) + + +@pytest.fixture +async def clean_functions(client): + # This function is a placeholder for the actual implementation + # that deletes all registered functions from the database. + functions, _ = await functions_rpc.list_functions( + app=client.app, pagination_limit=100, pagination_offset=0 + ) + for function in functions: + assert function.uid is not None + await functions_rpc.delete_function(app=client.app, function_id=function.uid) + + +async def test_register_function(client, mock_function: ProjectFunction): + # Register the function + registered_function = await functions_rpc.register_function( + app=client.app, function=mock_function + ) + assert registered_function.uid is not None + # Retrieve the function from the repository to verify it was saved + saved_function = await functions_rpc.get_function( + app=client.app, function_id=registered_function.uid + ) + + # Assert the saved function matches the input function + assert saved_function.uid is not None + assert saved_function.title == mock_function.title + assert saved_function.description == mock_function.description + + # Ensure saved_function is of type ProjectFunction before accessing project_id + assert isinstance(saved_function, ProjectFunction) + assert saved_function.project_id == mock_function.project_id + + # Assert the returned function matches the expected result + assert registered_function.title == mock_function.title + assert registered_function.description == mock_function.description + assert isinstance(registered_function, ProjectFunction) + assert registered_function.project_id == mock_function.project_id + + +async def test_get_function(client, mock_function: ProjectFunction): + # Register the function first + registered_function = await functions_rpc.register_function( + app=client.app, function=mock_function + ) + assert registered_function.uid is not None + + # Retrieve the function using its ID + retrieved_function = await functions_rpc.get_function( + app=client.app, function_id=registered_function.uid + ) + + # Assert the retrieved function matches the registered function + assert retrieved_function.uid == registered_function.uid + assert retrieved_function.title == registered_function.title + assert retrieved_function.description == registered_function.description + + # Ensure retrieved_function is of type ProjectFunction before accessing project_id + assert isinstance(retrieved_function, ProjectFunction) + assert isinstance(registered_function, ProjectFunction) + assert retrieved_function.project_id == registered_function.project_id + + +async def test_get_function_not_found(client): + # Attempt to retrieve a function that does not exist + with pytest.raises(FunctionIDNotFoundError): + await functions_rpc.get_function(app=client.app, function_id=uuid4()) + + +async def test_list_functions(client): + # Register a function first + mock_function = ProjectFunction( + title="Test Function", + description="A test function", + input_schema=JSONFunctionInputSchema(), + output_schema=JSONFunctionOutputSchema(), + project_id=uuid4(), + default_inputs=None, + ) + registered_function = await functions_rpc.register_function( + app=client.app, function=mock_function + ) + assert registered_function.uid is not None + + # List functions + functions, _ = await functions_rpc.list_functions( + app=client.app, pagination_limit=10, pagination_offset=0 + ) + + # Assert the list contains the registered function + assert len(functions) > 0 + assert any(f.uid == registered_function.uid for f in functions) + + +@pytest.mark.usefixtures("clean_functions") +async def test_list_functions_empty(client): + # List functions when none are registered + functions, _ = await functions_rpc.list_functions( + app=client.app, pagination_limit=10, pagination_offset=0 + ) + + # Assert the list is empty + assert len(functions) == 0 + + +@pytest.mark.usefixtures("clean_functions") +async def test_list_functions_with_pagination(client, mock_function): + # Register multiple functions + TOTAL_FUNCTIONS = 3 + for _ in range(TOTAL_FUNCTIONS): + await functions_rpc.register_function(app=client.app, function=mock_function) + + functions, page_info = await functions_rpc.list_functions( + app=client.app, pagination_limit=2, pagination_offset=0 + ) + + # List functions with pagination + functions, page_info = await functions_rpc.list_functions( + app=client.app, pagination_limit=2, pagination_offset=0 + ) + + # Assert the list contains the correct number of functions + assert len(functions) == 2 + assert page_info.count == 2 + assert page_info.total == TOTAL_FUNCTIONS + + # List the next page of functions + functions, page_info = await functions_rpc.list_functions( + app=client.app, pagination_limit=2, pagination_offset=2 + ) + + # Assert the list contains the correct number of functions + assert len(functions) == 1 + assert page_info.count == 1 + assert page_info.total == TOTAL_FUNCTIONS + + +async def test_get_function_input_schema(client, mock_function: ProjectFunction): + # Register the function first + registered_function = await functions_rpc.register_function( + app=client.app, function=mock_function + ) + assert registered_function.uid is not None + + # Retrieve the input schema using its ID + input_schema = await functions_rpc.get_function_input_schema( + app=client.app, function_id=registered_function.uid + ) + + # Assert the input schema matches the registered function's input schema + assert input_schema == registered_function.input_schema + + +async def test_get_function_output_schema(client, mock_function: ProjectFunction): + # Register the function first + registered_function = await functions_rpc.register_function( + app=client.app, function=mock_function + ) + assert registered_function.uid is not None + + # Retrieve the output schema using its ID + output_schema = await functions_rpc.get_function_output_schema( + app=client.app, function_id=registered_function.uid + ) + + # Assert the output schema matches the registered function's output schema + assert output_schema == registered_function.output_schema + + +async def test_delete_function(client, mock_function: ProjectFunction): + # Register the function first + registered_function = await functions_rpc.register_function( + app=client.app, function=mock_function + ) + assert registered_function.uid is not None + + # Delete the function using its ID + await functions_rpc.delete_function( + app=client.app, function_id=registered_function.uid + ) + + # Attempt to retrieve the deleted function + with pytest.raises(FunctionIDNotFoundError): + await functions_rpc.get_function( + app=client.app, function_id=registered_function.uid + ) + + +async def test_register_function_job(client, mock_function: ProjectFunction): + # Register the function first + registered_function = await functions_rpc.register_function( + app=client.app, function=mock_function + ) + assert registered_function.uid is not None + + function_job = ProjectFunctionJob( + function_uid=registered_function.uid, + title="Test Function Job", + description="A test function job", + project_job_id=uuid4(), + inputs={"input1": "value1"}, + outputs={"output1": "result1"}, + ) + + # Register the function job + registered_job = await functions_rpc.register_function_job( + app=client.app, function_job=function_job + ) + + # Assert the registered job matches the input job + assert registered_job.function_uid == function_job.function_uid + assert registered_job.inputs == function_job.inputs + assert registered_job.outputs == function_job.outputs + + +async def test_get_function_job(client, mock_function: ProjectFunction): + # Register the function first + registered_function = await functions_rpc.register_function( + app=client.app, function=mock_function + ) + assert registered_function.uid is not None + + function_job = ProjectFunctionJob( + function_uid=registered_function.uid, + title="Test Function Job", + description="A test function job", + project_job_id=uuid4(), + inputs={"input1": "value1"}, + outputs={"output1": "result1"}, + ) + + # Register the function job + registered_job = await functions_rpc.register_function_job( + app=client.app, function_job=function_job + ) + assert registered_job.uid is not None + + # Retrieve the function job using its ID + retrieved_job = await functions_rpc.get_function_job( + app=client.app, function_job_id=registered_job.uid + ) + + # Assert the retrieved job matches the registered job + assert retrieved_job.function_uid == registered_job.function_uid + assert retrieved_job.inputs == registered_job.inputs + assert retrieved_job.outputs == registered_job.outputs + + +async def test_get_function_job_not_found(client): + # Attempt to retrieve a function job that does not exist + with pytest.raises(FunctionJobIDNotFoundError): + await functions_rpc.get_function_job(app=client.app, function_job_id=uuid4()) + + +async def test_list_function_jobs(client, mock_function: ProjectFunction): + # Register the function first + registered_function = await functions_rpc.register_function( + app=client.app, function=mock_function + ) + assert registered_function.uid is not None + + function_job = ProjectFunctionJob( + function_uid=registered_function.uid, + title="Test Function Job", + description="A test function job", + project_job_id=uuid4(), + inputs={"input1": "value1"}, + outputs={"output1": "result1"}, + ) + + # Register the function job + registered_job = await functions_rpc.register_function_job( + app=client.app, function_job=function_job + ) + + # List function jobs + jobs, _ = await functions_rpc.list_function_jobs( + app=client.app, pagination_limit=10, pagination_offset=0 + ) + + # Assert the list contains the registered job + assert len(jobs) > 0 + assert any(j.uid == registered_job.uid for j in jobs) + + +async def test_delete_function_job(client, mock_function: ProjectFunction): + # Register the function first + registered_function = await functions_rpc.register_function( + app=client.app, function=mock_function + ) + assert registered_function.uid is not None + + function_job = ProjectFunctionJob( + function_uid=registered_function.uid, + title="Test Function Job", + description="A test function job", + project_job_id=uuid4(), + inputs={"input1": "value1"}, + outputs={"output1": "result1"}, + ) + + # Register the function job + registered_job = await functions_rpc.register_function_job( + app=client.app, function_job=function_job + ) + assert registered_job.uid is not None + + # Delete the function job using its ID + await functions_rpc.delete_function_job( + app=client.app, function_job_id=registered_job.uid + ) + + # Attempt to retrieve the deleted job + with pytest.raises(FunctionJobIDNotFoundError): + await functions_rpc.get_function_job( + app=client.app, function_job_id=registered_job.uid + ) + + +async def test_function_job_collection(client, mock_function: ProjectFunction): + # Register the function first + registered_function = await functions_rpc.register_function( + app=client.app, function=mock_function + ) + assert registered_function.uid is not None + + registered_function_job = ProjectFunctionJob( + function_uid=registered_function.uid, + title="Test Function Job", + description="A test function job", + project_job_id=uuid4(), + inputs={"input1": "value1"}, + outputs={"output1": "result1"}, + ) + # Register the function job + function_job_ids = [] + for _ in range(3): + registered_function_job = ProjectFunctionJob( + function_uid=registered_function.uid, + title="Test Function Job", + description="A test function job", + project_job_id=uuid4(), + inputs={"input1": "value1"}, + outputs={"output1": "result1"}, + ) + # Register the function job + registered_job = await functions_rpc.register_function_job( + app=client.app, function_job=registered_function_job + ) + assert registered_job.uid is not None + function_job_ids.append(registered_job.uid) + + function_job_collection = FunctionJobCollection( + title="Test Function Job Collection", + description="A test function job collection", + job_ids=function_job_ids, + ) + + # Register the function job collection + registered_collection = await functions_rpc.register_function_job_collection( + app=client.app, function_job_collection=function_job_collection + ) + assert registered_collection.uid is not None + + # Assert the registered collection matches the input collection + assert registered_collection.job_ids == function_job_ids + + await functions_rpc.delete_function_job_collection( + app=client.app, function_job_collection_id=registered_collection.uid + ) + # Attempt to retrieve the deleted collection + with pytest.raises(FunctionJobIDNotFoundError): + await functions_rpc.get_function_job( + app=client.app, function_job_id=registered_collection.uid + ) + + +async def test_list_function_job_collections(client, mock_function: ProjectFunction): + # Register the function first + registered_function = await functions_rpc.register_function( + app=client.app, function=mock_function + ) + assert registered_function.uid is not None + + # Create a function job collection + function_job_ids = [] + for _ in range(3): + registered_function_job = ProjectFunctionJob( + function_uid=registered_function.uid, + title="Test Function Job", + description="A test function job", + project_job_id=uuid4(), + inputs={"input1": "value1"}, + outputs={"output1": "result1"}, + ) + # Register the function job + registered_job = await functions_rpc.register_function_job( + app=client.app, function_job=registered_function_job + ) + assert registered_job.uid is not None + function_job_ids.append(registered_job.uid) + + function_job_collection = FunctionJobCollection( + title="Test Function Job Collection", + description="A test function job collection", + job_ids=function_job_ids, + ) + + # Register the function job collection + registered_collections = [ + await functions_rpc.register_function_job_collection( + app=client.app, function_job_collection=function_job_collection + ) + for _ in range(3) + ] + assert all( + registered_collection.uid is not None + for registered_collection in registered_collections + ) + + # List function job collections + collections, _ = await functions_rpc.list_function_job_collections( + app=client.app, pagination_limit=1, pagination_offset=1 + ) + + # Assert the list contains the registered collection + assert len(collections) == 1 + assert collections[0].uid == registered_collections[1].uid