diff --git a/api/api/openapi.bundle.yaml b/api/api/openapi.bundle.yaml index 2e5544f2e..afb58455d 100644 --- a/api/api/openapi.bundle.yaml +++ b/api/api/openapi.bundle.yaml @@ -242,7 +242,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/IdObject' + $ref: '#/components/schemas/JobId' description: Accepted. "400": description: Invalid ensembling job @@ -551,7 +551,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/IdObject_1' + $ref: '#/components/schemas/RouterId' description: OK "400": description: Invalid project_id or router_id @@ -682,7 +682,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/IdObject_1' + $ref: '#/components/schemas/RouterIdObject' description: OK "400": description: Invalid project_id or router_id @@ -863,9 +863,7 @@ paths: content: application/json: schema: - items: - $ref: '#/components/schemas/Event' - type: array + $ref: '#/components/schemas/RouterEvents' description: Get events "400": description: Invalid project_id or router_id @@ -1694,6 +1692,8 @@ components: - failed_submission - failed_building type: string + JobId: + $ref: '#/components/schemas/IdObject' IdObject: example: id: 0 @@ -1701,6 +1701,7 @@ components: id: format: int32 type: integer + type: object Id: format: int32 type: integer @@ -2520,7 +2521,7 @@ components: - environment_name - name type: object - IdObject_1: + RouterId: $ref: '#/components/schemas/IdObject' RouterIdAndVersion: example: @@ -2534,6 +2535,37 @@ components: format: int32 type: integer type: object + RouterIdObject: + example: + router_id: 0 + properties: + router_id: + format: int32 + type: integer + type: object + RouterEvents: + example: + events: + - event_type: info + updated_at: 2000-01-23T04:56:07.000+00:00 + stage: stage + created_at: 2000-01-23T04:56:07.000+00:00 + id: 0 + message: message + version: 6 + - event_type: info + updated_at: 2000-01-23T04:56:07.000+00:00 + stage: stage + created_at: 2000-01-23T04:56:07.000+00:00 + id: 0 + message: message + version: 6 + properties: + events: + items: + $ref: '#/components/schemas/Event' + type: array + type: object Event: example: event_type: info diff --git a/api/api/specs/common.yaml b/api/api/specs/common.yaml index d9b0bbd7d..9b0291eca 100644 --- a/api/api/specs/common.yaml +++ b/api/api/specs/common.yaml @@ -12,6 +12,7 @@ components: format: "int32" IdObject: + type: "object" properties: id: $ref: "#/components/schemas/Id" diff --git a/api/api/specs/jobs.yaml b/api/api/specs/jobs.yaml index 60f993259..f62bb080d 100644 --- a/api/api/specs/jobs.yaml +++ b/api/api/specs/jobs.yaml @@ -125,7 +125,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/IdObject" + $ref: "#/components/schemas/JobId" 400: description: "Invalid ensembling job" 404: @@ -133,7 +133,7 @@ paths: components: schemas: - IdObject: + JobId: $ref: "common.yaml#/components/schemas/IdObject" EnsemblingJob: description: A JSON object that represents an ensembling job for batch experiment use cases diff --git a/api/api/specs/routers.yaml b/api/api/specs/routers.yaml index 9255d5498..0d23d0c6a 100644 --- a/api/api/specs/routers.yaml +++ b/api/api/specs/routers.yaml @@ -152,7 +152,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/IdObject" + $ref: "#/components/schemas/RouterId" 400: description: "Invalid project_id or router_id" 404: @@ -212,7 +212,7 @@ paths: content: application/json: schema: - $ref: "#/components/schemas/IdObject" + $ref: "#/components/schemas/RouterIdObject" 400: description: "Invalid project_id or router_id" 404: @@ -380,9 +380,7 @@ paths: content: application/json: schema: - type: "array" - items: - $ref: "#/components/schemas/Event" + $ref: "#/components/schemas/RouterEvents" 400: description: "Invalid project_id or router_id" 404: @@ -390,8 +388,22 @@ paths: components: schemas: - IdObject: - $ref: "common.yaml#/components/schemas/IdObject" + RouterId: + $ref: "./common.yaml#/components/schemas/IdObject" + + RouterIdObject: + type: object + properties: + router_id: + $ref: "common.yaml#/components/schemas/Id" + + RouterEvents: + type: object + properties: + events: + type: "array" + items: + $ref: "#/components/schemas/Event" RouterIdAndVersion: type: object diff --git a/sdk/samples/router/create_from_existing_router.py b/sdk/samples/router/create_from_existing_router.py index b16aacb4c..cc55fef87 100644 --- a/sdk/samples/router/create_from_existing_router.py +++ b/sdk/samples/router/create_from_existing_router.py @@ -108,7 +108,6 @@ def main(turing_api: str, project: str): # Create an ensembler for the router ensembler = DockerRouterEnsemblerConfig( - id=1, image="ealen/echo-server:0.5.1", resource_request=ResourceRequest( min_replica=1, diff --git a/sdk/samples/router/general.py b/sdk/samples/router/general.py index 26cde612a..f6b408440 100644 --- a/sdk/samples/router/general.py +++ b/sdk/samples/router/general.py @@ -162,7 +162,6 @@ def main(turing_api: str, project: str): # Create an ensembler for the router ensembler = DockerRouterEnsemblerConfig( - id=1, image="ealen/echo-server:0.5.1", resource_request=ResourceRequest( min_replica=1, diff --git a/sdk/tests/conftest.py b/sdk/tests/conftest.py index ec82d58f7..e49260dfb 100644 --- a/sdk/tests/conftest.py +++ b/sdk/tests/conftest.py @@ -333,14 +333,24 @@ def generic_ensembler_docker_config(generic_resource_request, generic_env_var): ) -@pytest.fixture(params=["standard", "docker"]) -def ensembler(request, generic_ensembler_standard_config, generic_ensembler_docker_config): +@pytest.fixture +def generic_ensembler_pyfunc_config(generic_resource_request): + return turing.generated.models.EnsemblerPyfuncConfig( + project_id=77, + ensembler_id=11, + resource_request=generic_resource_request, + timeout="500ms" + ) + + +@pytest.fixture(params=["standard", "docker", "pyfunc"]) +def ensembler(request, generic_ensembler_standard_config, generic_ensembler_docker_config, generic_ensembler_pyfunc_config): ensembler_type = request.param return turing.generated.models.RouterEnsemblerConfig( - id=1, type=ensembler_type, standard_config=generic_ensembler_standard_config, docker_config=generic_ensembler_docker_config, + pyfunc_config=generic_ensembler_pyfunc_config, created_at=datetime.now() + timedelta(seconds=10), updated_at=datetime.now() + timedelta(seconds=10) ) @@ -362,6 +372,14 @@ def generic_docker_router_ensembler_config(generic_ensembler_docker_config): ) +@pytest.fixture +def generic_pyfunc_router_ensembler_config(generic_ensembler_pyfunc_config): + return turing.generated.models.RouterEnsemblerConfig( + type="pyfunc", + pyfunc_config=generic_ensembler_pyfunc_config + ) + + @pytest.fixture def generic_enricher(generic_resource_request, generic_env_var): return turing.generated.models.Enricher( @@ -508,7 +526,6 @@ def generic_router_config(): ] ), ensembler=DockerRouterEnsemblerConfig( - id=1, image="test.io/just-a-test/turing-ensembler:0.0.0-build.0", resource_request=ResourceRequest( min_replica=1, @@ -559,7 +576,7 @@ def generic_routers(project, num_routers, generic_router_status, generic_router_ @pytest.fixture def generic_events(): - return turing.generated.models.InlineResponse2002( + return turing.generated.models.RouterEvents( events=[ turing.generated.models.Event( created_at=datetime.now(), diff --git a/sdk/tests/router/config/router_ensembler_config_test.py b/sdk/tests/router/config/router_ensembler_config_test.py index d5de99d4d..3497c7b3c 100644 --- a/sdk/tests/router/config/router_ensembler_config_test.py +++ b/sdk/tests/router/config/router_ensembler_config_test.py @@ -4,6 +4,7 @@ from turing.router.config.common.env_var import EnvVar from turing.router.config.resource_request import ResourceRequest from turing.router.config.router_ensembler_config import (RouterEnsemblerConfig, + PyfuncRouterEnsemblerConfig, DockerRouterEnsemblerConfig, StandardRouterEnsemblerConfig, InvalidExperimentMappingException) @@ -65,11 +66,72 @@ def test_create_router_ensembler_config(id, type, standard_config, docker_config ).to_open_api() assert actual == request.getfixturevalue(expected) +@pytest.mark.parametrize( + "project_id,ensembler_id,resource_request,timeout,expected", [ + pytest.param( + 77, + 11, + ResourceRequest( + min_replica=1, + max_replica=3, + cpu_request='100m', + memory_request='512Mi' + ), + "500ms", + "generic_pyfunc_router_ensembler_config" + ) + ]) +def test_create_pyfunc_router_ensembler_config( + project_id, + ensembler_id, + resource_request, + timeout, + expected, + request +): + actual = PyfuncRouterEnsemblerConfig( + project_id=project_id, + ensembler_id=ensembler_id, + resource_request=resource_request, + timeout=timeout, + ).to_open_api() + assert actual == request.getfixturevalue(expected) + @pytest.mark.parametrize( - "id,image,resource_request,endpoint,timeout,port,env,service_account,expected", [ + "project_id,ensembler_id,resource_request,timeout,expected", [ + pytest.param( + 77, + 11, + ResourceRequest( + min_replica=1, + max_replica=3, + cpu_request='100m', + memory_request='512Mi' + ), + "500ks", + ApiValueError + ) + ]) +def test_create_pyfunc_router_ensembler_config_with_invalid_timeout( + project_id, + ensembler_id, + resource_request, + timeout, + expected +): + with pytest.raises(expected): + PyfuncRouterEnsemblerConfig( + project_id=project_id, + ensembler_id=ensembler_id, + resource_request=resource_request, + timeout=timeout, + ).to_open_api() + + +@pytest.mark.parametrize( + "image,resource_request,endpoint,timeout,port,env,service_account,expected", [ pytest.param( - 1, "test.io/just-a-test/turing-ensembler:0.0.0-build.0", ResourceRequest( min_replica=1, @@ -90,7 +152,6 @@ def test_create_router_ensembler_config(id, type, standard_config, docker_config ) ]) def test_create_docker_router_ensembler_config( - id, image, resource_request, endpoint, @@ -102,7 +163,6 @@ def test_create_docker_router_ensembler_config( request ): actual = DockerRouterEnsemblerConfig( - id=id, image=image, resource_request=resource_request, endpoint=endpoint, @@ -115,9 +175,8 @@ def test_create_docker_router_ensembler_config( @pytest.mark.parametrize( - "id,image,resource_request,endpoint,timeout,port,env,service_account,expected", [ + "image,resource_request,endpoint,timeout,port,env,service_account,expected", [ pytest.param( - 1, "#@!#!@#@!", ResourceRequest( min_replica=1, @@ -138,7 +197,6 @@ def test_create_docker_router_ensembler_config( ) ]) def test_create_docker_router_ensembler_config_with_invalid_image( - id, image, resource_request, endpoint, @@ -150,7 +208,6 @@ def test_create_docker_router_ensembler_config_with_invalid_image( ): with pytest.raises(expected): DockerRouterEnsemblerConfig( - id=id, image=image, resource_request=resource_request, endpoint=endpoint, @@ -162,9 +219,8 @@ def test_create_docker_router_ensembler_config_with_invalid_image( @pytest.mark.parametrize( - "id,image,resource_request,endpoint,timeout,port,env,service_account,expected", [ + "image,resource_request,endpoint,timeout,port,env,service_account,expected", [ pytest.param( - 1, "test.io/just-a-test/turing-ensembler:0.0.0-build.0", ResourceRequest( min_replica=1, @@ -185,7 +241,6 @@ def test_create_docker_router_ensembler_config_with_invalid_image( ) ]) def test_create_docker_router_ensembler_config_with_invalid_timeout( - id, image, resource_request, endpoint, @@ -197,7 +252,6 @@ def test_create_docker_router_ensembler_config_with_invalid_timeout( ): with pytest.raises(expected): DockerRouterEnsemblerConfig( - id=id, image=image, resource_request=resource_request, endpoint=endpoint, @@ -209,9 +263,8 @@ def test_create_docker_router_ensembler_config_with_invalid_timeout( @pytest.mark.parametrize( - "id,image,resource_request,endpoint,timeout,port,env,service_account,expected", [ + "image,resource_request,endpoint,timeout,port,env,service_account,expected", [ pytest.param( - 1, "test.io/just-a-test/turing-ensembler:0.0.0-build.0", ResourceRequest( min_replica=1, @@ -232,7 +285,6 @@ def test_create_docker_router_ensembler_config_with_invalid_timeout( ) ]) def test_create_docker_router_ensembler_config_with_invalid_env( - id, image, resource_request, endpoint, @@ -244,7 +296,6 @@ def test_create_docker_router_ensembler_config_with_invalid_env( ): with pytest.raises(expected): DockerRouterEnsemblerConfig( - id=id, image=image, resource_request=resource_request, endpoint=endpoint, @@ -256,9 +307,8 @@ def test_create_docker_router_ensembler_config_with_invalid_env( @pytest.mark.parametrize( - "id,experiment_mappings,expected", [ + "experiment_mappings,expected", [ pytest.param( - 1, [ { "experiment": "experiment-1", @@ -274,23 +324,21 @@ def test_create_docker_router_ensembler_config_with_invalid_env( "generic_standard_router_ensembler_config" ) ]) -def test_create_standard_router_ensembler_config(id, experiment_mappings, expected, request): +def test_create_standard_router_ensembler_config(experiment_mappings, expected, request): actual = StandardRouterEnsemblerConfig( - id=id, experiment_mappings=experiment_mappings ).to_open_api() assert actual == request.getfixturevalue(expected) @pytest.mark.parametrize( - "new_experiment_mappings, id,experiment_mappings,expected", [ + "new_experiment_mappings,experiment_mappings,expected", [ pytest.param( [ { "experiment": "wrong-experiment" } ], - 1, [ { "experiment": "experiment-1", @@ -313,7 +361,6 @@ def test_create_standard_router_ensembler_config(id, experiment_mappings, expect "route": 313 } ], - 1, [ { "experiment": "experiment-1", @@ -331,11 +378,9 @@ def test_create_standard_router_ensembler_config(id, experiment_mappings, expect ]) def test_set_standard_router_ensembler_config_with_invalid_experiment_mappings( new_experiment_mappings, - id, experiment_mappings, expected): actual = StandardRouterEnsemblerConfig( - id=id, experiment_mappings=experiment_mappings ) with pytest.raises(expected): @@ -343,7 +388,7 @@ def test_set_standard_router_ensembler_config_with_invalid_experiment_mappings( @pytest.mark.parametrize( - "new_experiment_mappings,id,experiment_mappings,expected", [ + "new_experiment_mappings,experiment_mappings,expected", [ pytest.param( [ { @@ -357,7 +402,6 @@ def test_set_standard_router_ensembler_config_with_invalid_experiment_mappings( "route": "route-2" }, ], - 1, [ { "experiment": "wrong-experiment", @@ -370,12 +414,10 @@ def test_set_standard_router_ensembler_config_with_invalid_experiment_mappings( ]) def test_set_standard_router_ensembler_config_with_valid_experiment_mappings( new_experiment_mappings, - id, experiment_mappings, expected, request): actual = StandardRouterEnsemblerConfig( - id=id, experiment_mappings=experiment_mappings ) actual.experiment_mappings = new_experiment_mappings diff --git a/sdk/tests/router/router_test.py b/sdk/tests/router/router_test.py index bed20f343..a3e673bb4 100644 --- a/sdk/tests/router/router_test.py +++ b/sdk/tests/router/router_test.py @@ -99,7 +99,7 @@ def test_create_router(turing_api, active_project, actual, expected, use_google_ "actual,expected", [ pytest.param( 1, - turing.generated.models.InlineResponse200(id=1) + turing.generated.models.IdObject(id=1) ) ] ) @@ -198,7 +198,7 @@ def test_deploy_router(turing_api, active_project, generic_router, generic_route base_router = turing.Router.from_open_api(generic_router) - expected = turing.generated.models.InlineResponse202( + expected = turing.generated.models.RouterIdAndVersion( router_id=1, version=1 ) @@ -223,7 +223,7 @@ def test_undeploy_router(turing_api, active_project, generic_router, generic_rou base_router = turing.Router.from_open_api(generic_router) - expected = turing.generated.models.InlineResponse2001( + expected = turing.generated.models.RouterIdObject( router_id=1, ) @@ -332,7 +332,7 @@ def test_delete_version(turing_api, active_project, generic_router, use_google_o expected_router_id = 1 expected_version = 1 - expected = turing.generated.models.InlineResponse202( + expected = turing.generated.models.RouterIdAndVersion( router_id=expected_router_id, version=expected_version ) @@ -359,7 +359,7 @@ def test_deploy_version(turing_api, active_project, generic_router, use_google_o expected_router_id = 1 expected_version = 1 - expected = turing.generated.models.InlineResponse202( + expected = turing.generated.models.RouterIdAndVersion( router_id=expected_router_id, version=expected_version ) @@ -393,7 +393,7 @@ def test_get_events_list(turing_api, active_project, generic_router, generic_eve ) response = base_router.get_events() - expected_events = generic_events.get('events') + expected_events = generic_events['events'] assert len(response) == len(expected_events) diff --git a/sdk/turing/generated/api/router_api.py b/sdk/turing/generated/api/router_api.py index d48a72039..7e10488d6 100644 --- a/sdk/turing/generated/api/router_api.py +++ b/sdk/turing/generated/api/router_api.py @@ -21,12 +21,12 @@ none_type, validate_and_convert_types ) -from turing.generated.model.inline_response200 import InlineResponse200 -from turing.generated.model.inline_response2001 import InlineResponse2001 -from turing.generated.model.inline_response2002 import InlineResponse2002 -from turing.generated.model.inline_response202 import InlineResponse202 +from turing.generated.model.id_object import IdObject from turing.generated.model.router_config import RouterConfig from turing.generated.model.router_details import RouterDetails +from turing.generated.model.router_events import RouterEvents +from turing.generated.model.router_id_and_version import RouterIdAndVersion +from turing.generated.model.router_id_object import RouterIdObject from turing.generated.model.router_version import RouterVersion @@ -325,7 +325,7 @@ def __projects_project_id_routers_router_id_delete( async_req (bool): execute request asynchronously Returns: - InlineResponse200 + IdObject If the method is called asynchronously, returns the request thread. """ @@ -356,7 +356,7 @@ def __projects_project_id_routers_router_id_delete( self.projects_project_id_routers_router_id_delete = _Endpoint( settings={ - 'response_type': (InlineResponse200,), + 'response_type': (IdObject,), 'auth': [], 'endpoint_path': '/projects/{project_id}/routers/{router_id}', 'operation_id': 'projects_project_id_routers_router_id_delete', @@ -451,7 +451,7 @@ def __projects_project_id_routers_router_id_deploy_post( async_req (bool): execute request asynchronously Returns: - InlineResponse202 + RouterIdAndVersion If the method is called asynchronously, returns the request thread. """ @@ -482,7 +482,7 @@ def __projects_project_id_routers_router_id_deploy_post( self.projects_project_id_routers_router_id_deploy_post = _Endpoint( settings={ - 'response_type': (InlineResponse202,), + 'response_type': (RouterIdAndVersion,), 'auth': [], 'endpoint_path': '/projects/{project_id}/routers/{router_id}/deploy', 'operation_id': 'projects_project_id_routers_router_id_deploy_post', @@ -577,7 +577,7 @@ def __projects_project_id_routers_router_id_events_get( async_req (bool): execute request asynchronously Returns: - InlineResponse2002 + RouterEvents If the method is called asynchronously, returns the request thread. """ @@ -608,7 +608,7 @@ def __projects_project_id_routers_router_id_events_get( self.projects_project_id_routers_router_id_events_get = _Endpoint( settings={ - 'response_type': (InlineResponse2002,), + 'response_type': (RouterEvents,), 'auth': [], 'endpoint_path': '/projects/{project_id}/routers/{router_id}/events', 'operation_id': 'projects_project_id_routers_router_id_events_get', @@ -966,7 +966,7 @@ def __projects_project_id_routers_router_id_undeploy_post( async_req (bool): execute request asynchronously Returns: - InlineResponse2001 + RouterIdObject If the method is called asynchronously, returns the request thread. """ @@ -997,7 +997,7 @@ def __projects_project_id_routers_router_id_undeploy_post( self.projects_project_id_routers_router_id_undeploy_post = _Endpoint( settings={ - 'response_type': (InlineResponse2001,), + 'response_type': (RouterIdObject,), 'auth': [], 'endpoint_path': '/projects/{project_id}/routers/{router_id}/undeploy', 'operation_id': 'projects_project_id_routers_router_id_undeploy_post', @@ -1220,7 +1220,7 @@ def __projects_project_id_routers_router_id_versions_version_delete( async_req (bool): execute request asynchronously Returns: - InlineResponse202 + RouterIdAndVersion If the method is called asynchronously, returns the request thread. """ @@ -1253,7 +1253,7 @@ def __projects_project_id_routers_router_id_versions_version_delete( self.projects_project_id_routers_router_id_versions_version_delete = _Endpoint( settings={ - 'response_type': (InlineResponse202,), + 'response_type': (RouterIdAndVersion,), 'auth': [], 'endpoint_path': '/projects/{project_id}/routers/{router_id}/versions/{version}', 'operation_id': 'projects_project_id_routers_router_id_versions_version_delete', @@ -1356,7 +1356,7 @@ def __projects_project_id_routers_router_id_versions_version_deploy_post( async_req (bool): execute request asynchronously Returns: - InlineResponse202 + RouterIdAndVersion If the method is called asynchronously, returns the request thread. """ @@ -1389,7 +1389,7 @@ def __projects_project_id_routers_router_id_versions_version_deploy_post( self.projects_project_id_routers_router_id_versions_version_deploy_post = _Endpoint( settings={ - 'response_type': (InlineResponse202,), + 'response_type': (RouterIdAndVersion,), 'auth': [], 'endpoint_path': '/projects/{project_id}/routers/{router_id}/versions/{version}/deploy', 'operation_id': 'projects_project_id_routers_router_id_versions_version_deploy_post', diff --git a/sdk/turing/generated/model/ensembler_infra_config.py b/sdk/turing/generated/model/ensembler_infra_config.py index 0b7129933..cdaed0732 100644 --- a/sdk/turing/generated/model/ensembler_infra_config.py +++ b/sdk/turing/generated/model/ensembler_infra_config.py @@ -83,6 +83,7 @@ def openapi_types(): 'ensembler_name': (str,), # noqa: E501 'service_account_name': (str,), # noqa: E501 'resources': (EnsemblingResources,), # noqa: E501 + 'run_id': (str,), # noqa: E501 'env': ([EnvVar],), # noqa: E501 } @@ -96,6 +97,7 @@ def discriminator(): 'ensembler_name': 'ensembler_name', # noqa: E501 'service_account_name': 'service_account_name', # noqa: E501 'resources': 'resources', # noqa: E501 + 'run_id': 'run_id', # noqa: E501 'env': 'env', # noqa: E501 } @@ -149,6 +151,7 @@ def __init__(self, *args, **kwargs): # noqa: E501 ensembler_name (str): [optional] # noqa: E501 service_account_name (str): [optional] # noqa: E501 resources (EnsemblingResources): [optional] # noqa: E501 + run_id (str): [optional] # noqa: E501 env ([EnvVar]): [optional] # noqa: E501 """ diff --git a/sdk/turing/generated/model/inline_response200.py b/sdk/turing/generated/model/ensembler_pyfunc_config.py similarity index 83% rename from sdk/turing/generated/model/inline_response200.py rename to sdk/turing/generated/model/ensembler_pyfunc_config.py index 15873ee78..554b38712 100644 --- a/sdk/turing/generated/model/inline_response200.py +++ b/sdk/turing/generated/model/ensembler_pyfunc_config.py @@ -26,8 +26,12 @@ validate_get_composed_info, ) +def lazy_import(): + from turing.generated.model.resource_request import ResourceRequest + globals()['ResourceRequest'] = ResourceRequest -class InlineResponse200(ModelNormal): + +class EnsemblerPyfuncConfig(ModelNormal): """NOTE: This class is auto generated by OpenAPI Generator. Ref: https://openapi-generator.tech @@ -55,11 +59,16 @@ class InlineResponse200(ModelNormal): } validations = { + ('timeout',): { + 'regex': { + 'pattern': r'^[0-9]+(ms|s|m|h)$', # noqa: E501 + }, + }, } additional_properties_type = None - _nullable = False + _nullable = True @cached_property def openapi_types(): @@ -71,8 +80,12 @@ def openapi_types(): openapi_types (dict): The key is attribute name and the value is attribute type. """ + lazy_import() return { - 'id': (int,), # noqa: E501 + 'project_id': (int,), # noqa: E501 + 'ensembler_id': (int,), # noqa: E501 + 'resource_request': (ResourceRequest,), # noqa: E501 + 'timeout': (str,), # noqa: E501 } @cached_property @@ -81,7 +94,10 @@ def discriminator(): attribute_map = { - 'id': 'id', # noqa: E501 + 'project_id': 'project_id', # noqa: E501 + 'ensembler_id': 'ensembler_id', # noqa: E501 + 'resource_request': 'resource_request', # noqa: E501 + 'timeout': 'timeout', # noqa: E501 } _composed_schemas = {} @@ -96,8 +112,14 @@ def discriminator(): ]) @convert_js_args_to_python_args - def __init__(self, *args, **kwargs): # noqa: E501 - """InlineResponse200 - a model defined in OpenAPI + def __init__(self, project_id, ensembler_id, resource_request, timeout, *args, **kwargs): # noqa: E501 + """EnsemblerPyfuncConfig - a model defined in OpenAPI + + Args: + project_id (int): + ensembler_id (int): + resource_request (ResourceRequest): + timeout (str): Keyword Args: _check_type (bool): if True, values for parameters in openapi_types @@ -130,7 +152,6 @@ def __init__(self, *args, **kwargs): # noqa: E501 Animal class but this time we won't travel through its discriminator because we passed in _visited_composed_classes = (Animal,) - id (int): [optional] # noqa: E501 """ _check_type = kwargs.pop('_check_type', True) @@ -156,6 +177,10 @@ def __init__(self, *args, **kwargs): # noqa: E501 self._configuration = _configuration self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + self.project_id = project_id + self.ensembler_id = ensembler_id + self.resource_request = resource_request + self.timeout = timeout for var_name, var_value in kwargs.items(): if var_name not in self.attribute_map and \ self._configuration is not None and \ diff --git a/sdk/turing/generated/model/job_id.py b/sdk/turing/generated/model/job_id.py new file mode 100644 index 000000000..e8c622867 --- /dev/null +++ b/sdk/turing/generated/model/job_id.py @@ -0,0 +1,179 @@ +""" + Turing Minimal Openapi Spec for SDK + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) # noqa: E501 + + The version of the OpenAPI document: 0.0.1 + Generated by: https://openapi-generator.tech +""" + + +import re # noqa: F401 +import sys # noqa: F401 + +from turing.generated.model_utils import ( # noqa: F401 + ApiTypeError, + ModelComposed, + ModelNormal, + ModelSimple, + cached_property, + change_keys_js_to_python, + convert_js_args_to_python_args, + date, + datetime, + file_type, + none_type, + validate_get_composed_info, +) + + +class JobId(ModelSimple): + """NOTE: This class is auto generated by OpenAPI Generator. + Ref: https://openapi-generator.tech + + Do not edit the class manually. + + Attributes: + allowed_values (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + with a capitalized key describing the allowed value and an allowed + value. These dicts store the allowed enum values. + validations (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + that stores validations for max_length, min_length, max_items, + min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum, + inclusive_minimum, and regex. + additional_properties_type (tuple): A tuple of classes accepted + as additional properties values. + """ + + allowed_values = { + } + + validations = { + } + + additional_properties_type = None + + _nullable = False + + @cached_property + def openapi_types(): + """ + This must be a method because a model may have properties that are + of type self, this must run after the class is loaded + + Returns + openapi_types (dict): The key is attribute name + and the value is attribute type. + """ + return { + 'value': (IdObject,), + } + + @cached_property + def discriminator(): + return None + + + attribute_map = {} + + _composed_schemas = None + + required_properties = set([ + '_data_store', + '_check_type', + '_spec_property_naming', + '_path_to_item', + '_configuration', + '_visited_composed_classes', + ]) + + @convert_js_args_to_python_args + def __init__(self, *args, **kwargs): + """JobId - a model defined in OpenAPI + + Note that value can be passed either in args or in kwargs, but not in both. + + Args: + args[0] (IdObject): # noqa: E501 + + Keyword Args: + value (IdObject): # noqa: E501 + _check_type (bool): if True, values for parameters in openapi_types + will be type checked and a TypeError will be + raised if the wrong type is input. + Defaults to True + _path_to_item (tuple/list): This is a list of keys or values to + drill down to the model in received_data + when deserializing a response + _spec_property_naming (bool): True if the variable names in the input data + are serialized names, as specified in the OpenAPI document. + False if the variable names in the input data + are pythonic names, e.g. snake case (default) + _configuration (Configuration): the instance to use when + deserializing a file_type parameter. + If passed, type conversion is attempted + If omitted no type conversion is done. + _visited_composed_classes (tuple): This stores a tuple of + classes that we have traveled through so that + if we see that class again we will not use its + discriminator again. + When traveling through a discriminator, the + composed schema that is + is traveled through is added to this set. + For example if Animal has a discriminator + petType and we pass in "Dog", and the class Dog + allOf includes Animal, we move through Animal + once using the discriminator, and pick Dog. + Then in Dog, we will make an instance of the + Animal class but this time we won't travel + through its discriminator because we passed in + _visited_composed_classes = (Animal,) + """ + # required up here when default value is not given + _path_to_item = kwargs.pop('_path_to_item', ()) + + if 'value' in kwargs: + value = kwargs.pop('value') + elif args: + args = list(args) + value = args.pop(0) + else: + raise ApiTypeError( + "value is required, but not passed in args or kwargs and doesn't have default", + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + _check_type = kwargs.pop('_check_type', True) + _spec_property_naming = kwargs.pop('_spec_property_naming', False) + _configuration = kwargs.pop('_configuration', None) + _visited_composed_classes = kwargs.pop('_visited_composed_classes', ()) + + if args: + raise ApiTypeError( + "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % ( + args, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + self._data_store = {} + self._check_type = _check_type + self._spec_property_naming = _spec_property_naming + self._path_to_item = _path_to_item + self._configuration = _configuration + self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + self.value = value + if kwargs: + raise ApiTypeError( + "Invalid named arguments=%s passed to %s. Remove those invalid named arguments." % ( + kwargs, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) diff --git a/sdk/turing/generated/model/router_ensembler_config.py b/sdk/turing/generated/model/router_ensembler_config.py index 60288d27a..77226556e 100644 --- a/sdk/turing/generated/model/router_ensembler_config.py +++ b/sdk/turing/generated/model/router_ensembler_config.py @@ -28,8 +28,10 @@ def lazy_import(): from turing.generated.model.ensembler_docker_config import EnsemblerDockerConfig + from turing.generated.model.ensembler_pyfunc_config import EnsemblerPyfuncConfig from turing.generated.model.ensembler_standard_config import EnsemblerStandardConfig globals()['EnsemblerDockerConfig'] = EnsemblerDockerConfig + globals()['EnsemblerPyfuncConfig'] = EnsemblerPyfuncConfig globals()['EnsemblerStandardConfig'] = EnsemblerStandardConfig @@ -61,6 +63,7 @@ class RouterEnsemblerConfig(ModelNormal): ('type',): { 'STANDARD': "standard", 'DOCKER': "docker", + 'PYFUNC': "pyfunc", }, } @@ -87,6 +90,7 @@ def openapi_types(): 'id': (int,), # noqa: E501 'standard_config': (EnsemblerStandardConfig,), # noqa: E501 'docker_config': (EnsemblerDockerConfig,), # noqa: E501 + 'pyfunc_config': (EnsemblerPyfuncConfig,), # noqa: E501 'created_at': (datetime,), # noqa: E501 'updated_at': (datetime,), # noqa: E501 } @@ -101,6 +105,7 @@ def discriminator(): 'id': 'id', # noqa: E501 'standard_config': 'standard_config', # noqa: E501 'docker_config': 'docker_config', # noqa: E501 + 'pyfunc_config': 'pyfunc_config', # noqa: E501 'created_at': 'created_at', # noqa: E501 'updated_at': 'updated_at', # noqa: E501 } @@ -157,6 +162,7 @@ def __init__(self, type, *args, **kwargs): # noqa: E501 id (int): [optional] # noqa: E501 standard_config (EnsemblerStandardConfig): [optional] # noqa: E501 docker_config (EnsemblerDockerConfig): [optional] # noqa: E501 + pyfunc_config (EnsemblerPyfuncConfig): [optional] # noqa: E501 created_at (datetime): [optional] # noqa: E501 updated_at (datetime): [optional] # noqa: E501 """ diff --git a/sdk/turing/generated/model/inline_response2002.py b/sdk/turing/generated/model/router_events.py similarity index 98% rename from sdk/turing/generated/model/inline_response2002.py rename to sdk/turing/generated/model/router_events.py index 3f149ebee..1fb485942 100644 --- a/sdk/turing/generated/model/inline_response2002.py +++ b/sdk/turing/generated/model/router_events.py @@ -31,7 +31,7 @@ def lazy_import(): globals()['Event'] = Event -class InlineResponse2002(ModelNormal): +class RouterEvents(ModelNormal): """NOTE: This class is auto generated by OpenAPI Generator. Ref: https://openapi-generator.tech @@ -102,7 +102,7 @@ def discriminator(): @convert_js_args_to_python_args def __init__(self, *args, **kwargs): # noqa: E501 - """InlineResponse2002 - a model defined in OpenAPI + """RouterEvents - a model defined in OpenAPI Keyword Args: _check_type (bool): if True, values for parameters in openapi_types diff --git a/sdk/turing/generated/model/router_id.py b/sdk/turing/generated/model/router_id.py new file mode 100644 index 000000000..fffce52da --- /dev/null +++ b/sdk/turing/generated/model/router_id.py @@ -0,0 +1,179 @@ +""" + Turing Minimal Openapi Spec for SDK + + No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator) # noqa: E501 + + The version of the OpenAPI document: 0.0.1 + Generated by: https://openapi-generator.tech +""" + + +import re # noqa: F401 +import sys # noqa: F401 + +from turing.generated.model_utils import ( # noqa: F401 + ApiTypeError, + ModelComposed, + ModelNormal, + ModelSimple, + cached_property, + change_keys_js_to_python, + convert_js_args_to_python_args, + date, + datetime, + file_type, + none_type, + validate_get_composed_info, +) + + +class RouterId(ModelSimple): + """NOTE: This class is auto generated by OpenAPI Generator. + Ref: https://openapi-generator.tech + + Do not edit the class manually. + + Attributes: + allowed_values (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + with a capitalized key describing the allowed value and an allowed + value. These dicts store the allowed enum values. + validations (dict): The key is the tuple path to the attribute + and the for var_name this is (var_name,). The value is a dict + that stores validations for max_length, min_length, max_items, + min_items, exclusive_maximum, inclusive_maximum, exclusive_minimum, + inclusive_minimum, and regex. + additional_properties_type (tuple): A tuple of classes accepted + as additional properties values. + """ + + allowed_values = { + } + + validations = { + } + + additional_properties_type = None + + _nullable = False + + @cached_property + def openapi_types(): + """ + This must be a method because a model may have properties that are + of type self, this must run after the class is loaded + + Returns + openapi_types (dict): The key is attribute name + and the value is attribute type. + """ + return { + 'value': (IdObject,), + } + + @cached_property + def discriminator(): + return None + + + attribute_map = {} + + _composed_schemas = None + + required_properties = set([ + '_data_store', + '_check_type', + '_spec_property_naming', + '_path_to_item', + '_configuration', + '_visited_composed_classes', + ]) + + @convert_js_args_to_python_args + def __init__(self, *args, **kwargs): + """RouterId - a model defined in OpenAPI + + Note that value can be passed either in args or in kwargs, but not in both. + + Args: + args[0] (IdObject): # noqa: E501 + + Keyword Args: + value (IdObject): # noqa: E501 + _check_type (bool): if True, values for parameters in openapi_types + will be type checked and a TypeError will be + raised if the wrong type is input. + Defaults to True + _path_to_item (tuple/list): This is a list of keys or values to + drill down to the model in received_data + when deserializing a response + _spec_property_naming (bool): True if the variable names in the input data + are serialized names, as specified in the OpenAPI document. + False if the variable names in the input data + are pythonic names, e.g. snake case (default) + _configuration (Configuration): the instance to use when + deserializing a file_type parameter. + If passed, type conversion is attempted + If omitted no type conversion is done. + _visited_composed_classes (tuple): This stores a tuple of + classes that we have traveled through so that + if we see that class again we will not use its + discriminator again. + When traveling through a discriminator, the + composed schema that is + is traveled through is added to this set. + For example if Animal has a discriminator + petType and we pass in "Dog", and the class Dog + allOf includes Animal, we move through Animal + once using the discriminator, and pick Dog. + Then in Dog, we will make an instance of the + Animal class but this time we won't travel + through its discriminator because we passed in + _visited_composed_classes = (Animal,) + """ + # required up here when default value is not given + _path_to_item = kwargs.pop('_path_to_item', ()) + + if 'value' in kwargs: + value = kwargs.pop('value') + elif args: + args = list(args) + value = args.pop(0) + else: + raise ApiTypeError( + "value is required, but not passed in args or kwargs and doesn't have default", + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + _check_type = kwargs.pop('_check_type', True) + _spec_property_naming = kwargs.pop('_spec_property_naming', False) + _configuration = kwargs.pop('_configuration', None) + _visited_composed_classes = kwargs.pop('_visited_composed_classes', ()) + + if args: + raise ApiTypeError( + "Invalid positional arguments=%s passed to %s. Remove those invalid positional arguments." % ( + args, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) + + self._data_store = {} + self._check_type = _check_type + self._spec_property_naming = _spec_property_naming + self._path_to_item = _path_to_item + self._configuration = _configuration + self._visited_composed_classes = _visited_composed_classes + (self.__class__,) + self.value = value + if kwargs: + raise ApiTypeError( + "Invalid named arguments=%s passed to %s. Remove those invalid named arguments." % ( + kwargs, + self.__class__.__name__, + ), + path_to_item=_path_to_item, + valid_classes=(self.__class__,), + ) diff --git a/sdk/turing/generated/model/inline_response202.py b/sdk/turing/generated/model/router_id_and_version.py similarity index 98% rename from sdk/turing/generated/model/inline_response202.py rename to sdk/turing/generated/model/router_id_and_version.py index 916edfdf7..9604c3752 100644 --- a/sdk/turing/generated/model/inline_response202.py +++ b/sdk/turing/generated/model/router_id_and_version.py @@ -27,7 +27,7 @@ ) -class InlineResponse202(ModelNormal): +class RouterIdAndVersion(ModelNormal): """NOTE: This class is auto generated by OpenAPI Generator. Ref: https://openapi-generator.tech @@ -99,7 +99,7 @@ def discriminator(): @convert_js_args_to_python_args def __init__(self, *args, **kwargs): # noqa: E501 - """InlineResponse202 - a model defined in OpenAPI + """RouterIdAndVersion - a model defined in OpenAPI Keyword Args: _check_type (bool): if True, values for parameters in openapi_types diff --git a/sdk/turing/generated/model/inline_response2001.py b/sdk/turing/generated/model/router_id_object.py similarity index 98% rename from sdk/turing/generated/model/inline_response2001.py rename to sdk/turing/generated/model/router_id_object.py index 00f4cb177..3b494df0f 100644 --- a/sdk/turing/generated/model/inline_response2001.py +++ b/sdk/turing/generated/model/router_id_object.py @@ -27,7 +27,7 @@ ) -class InlineResponse2001(ModelNormal): +class RouterIdObject(ModelNormal): """NOTE: This class is auto generated by OpenAPI Generator. Ref: https://openapi-generator.tech @@ -97,7 +97,7 @@ def discriminator(): @convert_js_args_to_python_args def __init__(self, *args, **kwargs): # noqa: E501 - """InlineResponse2001 - a model defined in OpenAPI + """RouterIdObject - a model defined in OpenAPI Keyword Args: _check_type (bool): if True, values for parameters in openapi_types diff --git a/sdk/turing/generated/models/__init__.py b/sdk/turing/generated/models/__init__.py index 33ee4b7e1..b04219a04 100644 --- a/sdk/turing/generated/models/__init__.py +++ b/sdk/turing/generated/models/__init__.py @@ -24,6 +24,7 @@ from turing.generated.model.ensembler_docker_config import EnsemblerDockerConfig from turing.generated.model.ensembler_infra_config import EnsemblerInfraConfig from turing.generated.model.ensembler_job_status import EnsemblerJobStatus +from turing.generated.model.ensembler_pyfunc_config import EnsemblerPyfuncConfig from turing.generated.model.ensembler_standard_config import EnsemblerStandardConfig from turing.generated.model.ensembler_standard_config_experiment_mappings import EnsemblerStandardConfigExperimentMappings from turing.generated.model.ensembler_type import EnsemblerType @@ -51,10 +52,7 @@ from turing.generated.model.generic_ensembler import GenericEnsembler from turing.generated.model.generic_sink import GenericSink from turing.generated.model.id_object import IdObject -from turing.generated.model.inline_response200 import InlineResponse200 -from turing.generated.model.inline_response2001 import InlineResponse2001 -from turing.generated.model.inline_response2002 import InlineResponse2002 -from turing.generated.model.inline_response202 import InlineResponse202 +from turing.generated.model.job_id import JobId from turing.generated.model.kafka_config import KafkaConfig from turing.generated.model.label import Label from turing.generated.model.log_level import LogLevel @@ -72,6 +70,10 @@ from turing.generated.model.router_details import RouterDetails from turing.generated.model.router_details_all_of import RouterDetailsAllOf from turing.generated.model.router_ensembler_config import RouterEnsemblerConfig +from turing.generated.model.router_events import RouterEvents +from turing.generated.model.router_id import RouterId +from turing.generated.model.router_id_and_version import RouterIdAndVersion +from turing.generated.model.router_id_object import RouterIdObject from turing.generated.model.router_status import RouterStatus from turing.generated.model.router_version import RouterVersion from turing.generated.model.router_version_log_config import RouterVersionLogConfig diff --git a/sdk/turing/router/config/router_ensembler_config.py b/sdk/turing/router/config/router_ensembler_config.py index 35a487ba0..ccac92522 100644 --- a/sdk/turing/router/config/router_ensembler_config.py +++ b/sdk/turing/router/config/router_ensembler_config.py @@ -21,17 +21,20 @@ class RouterEnsemblerConfig: id: int = None standard_config: turing.generated.models.EnsemblerStandardConfig = None docker_config: turing.generated.models.EnsemblerDockerConfig = None + pyfunc_config: turing.generated.models.EnsemblerPyfuncConfig = None def __init__(self, type: str, id: int = None, standard_config: turing.generated.models.EnsemblerStandardConfig = None, docker_config: turing.generated.models.EnsemblerDockerConfig = None, + pyfunc_config: turing.generated.models.EnsemblerPyfuncConfig = None, **kwargs): self.id = id self.type = type self.standard_config = standard_config self.docker_config = docker_config + self.pyfunc_config = pyfunc_config @property def id(self) -> int: @@ -47,7 +50,7 @@ def type(self) -> str: @type.setter def type(self, type: str): - assert type in {'standard', 'docker'} + assert type in {"standard", "docker", "pyfunc"} self._type = type @property @@ -59,9 +62,9 @@ def standard_config(self, standard_config: Union[turing.generated.models.Ensembl if isinstance(standard_config, turing.generated.models.EnsemblerStandardConfig): self._standard_config = standard_config elif isinstance(standard_config, dict): - standard_config['experiment_mappings'] = [ + standard_config["experiment_mappings"] = [ turing.generated.models.EnsemblerStandardConfigExperimentMappings(**mapping) - for mapping in standard_config['experiment_mappings'] + for mapping in standard_config["experiment_mappings"] ] self._standard_config = turing.generated.models.EnsemblerStandardConfig(**standard_config) else: @@ -76,15 +79,32 @@ def docker_config(self, docker_config: turing.generated.models.EnsemblerDockerCo if isinstance(docker_config, turing.generated.models.EnsemblerDockerConfig): self._docker_config = docker_config elif isinstance(docker_config, dict): - docker_config['resource_request'] = \ - turing.generated.models.ResourceRequest(**docker_config['resource_request']) - docker_config['env'] = [turing.generated.models.EnvVar(**env_var) for env_var in docker_config['env']] + docker_config["resource_request"] = \ + turing.generated.models.ResourceRequest(**docker_config["resource_request"]) + docker_config["env"] = [turing.generated.models.EnvVar(**env_var) for env_var in docker_config["env"]] self._docker_config = turing.generated.models.EnsemblerDockerConfig( **docker_config ) else: self._docker_config = docker_config + @property + def pyfunc_config(self) -> turing.generated.models.EnsemblerPyfuncConfig: + return self._pyfunc_config + + @pyfunc_config.setter + def pyfunc_config(self, pyfunc_config: turing.generated.models.EnsemblerPyfuncConfig): + if isinstance(pyfunc_config, turing.generated.models.EnsemblerPyfuncConfig): + self._pyfunc_config = pyfunc_config + elif isinstance(pyfunc_config, dict): + pyfunc_config["resource_request"] = \ + turing.generated.models.ResourceRequest(**pyfunc_config["resource_request"]) + self._pyfunc_config = turing.generated.models.EnsemblerPyfuncConfig( + **pyfunc_config + ) + else: + self._pyfunc_config = pyfunc_config + def to_open_api(self) -> OpenApiModel: kwargs = {} @@ -92,6 +112,8 @@ def to_open_api(self) -> OpenApiModel: kwargs["standard_config"] = self.standard_config if self.docker_config is not None: kwargs["docker_config"] = self.docker_config + if self.pyfunc_config is not None: + kwargs["pyfunc_config"] = self.pyfunc_config return turing.generated.models.RouterEnsemblerConfig( type=self.type, @@ -99,10 +121,72 @@ def to_open_api(self) -> OpenApiModel: ) +@dataclass +class PyfuncRouterEnsemblerConfig(RouterEnsemblerConfig): + def __init__(self, + project_id: int, + ensembler_id: int, + timeout: str, + resource_request: ResourceRequest): + """ + Method to create a new Pyfunc ensembler + + :param project_id: project id of the current project + :param ensembler_id: ensembler_id of the ensembler + :param resource_request: ResourceRequest instance containing configs related to the resources required + :param timeout: request timeout which when exceeded, the request to the ensembler will be terminated + """ + self.project_id = project_id + self.ensembler_id = ensembler_id + self.resource_request = resource_request + self.timeout = timeout + super().__init__(type="pyfunc") + + @property + def project_id(self) -> int: + return self._project_id + + @project_id.setter + def project_id(self, project_id: int): + self._project_id = project_id + + @property + def ensembler_id(self) -> int: + return self._ensembler_id + + @ensembler_id.setter + def ensembler_id(self, ensembler_id: int): + self._ensembler_id = ensembler_id + + @property + def resource_request(self) -> ResourceRequest: + return self._resource_request + + @resource_request.setter + def resource_request(self, resource_request: ResourceRequest): + self._resource_request = resource_request + + @property + def timeout(self) -> str: + return self._timeout + + @timeout.setter + def timeout(self, timeout: str): + self._timeout = timeout + + def to_open_api(self) -> OpenApiModel: + self.pyfunc_config = turing.generated.models.EnsemblerPyfuncConfig( + project_id=self.project_id, + ensembler_id=self.ensembler_id, + resource_request=self.resource_request.to_open_api(), + timeout=self.timeout, + ) + return super().to_open_api() + + @dataclass class DockerRouterEnsemblerConfig(RouterEnsemblerConfig): def __init__(self, - id: int, image: str, resource_request: ResourceRequest, endpoint: str, @@ -113,7 +197,6 @@ def __init__(self, """ Method to create a new Docker ensembler - :param id: id of the ensembler :param image: registry and name of the image :param resource_request: ResourceRequest instance containing configs related to the resources required :param endpoint: endpoint URL of the ensembler @@ -129,7 +212,7 @@ def __init__(self, self.port = port self.env = env self.service_account = service_account - super().__init__(id=id, type="docker") + super().__init__(type="docker") @property def image(self) -> str: @@ -192,7 +275,7 @@ def to_open_api(self) -> OpenApiModel: kwargs = {} if self.service_account is not None: - kwargs['service_account'] = self.service_account + kwargs["service_account"] = self.service_account self.docker_config = turing.generated.models.EnsemblerDockerConfig( image=self.image, @@ -209,16 +292,14 @@ def to_open_api(self) -> OpenApiModel: @dataclass class StandardRouterEnsemblerConfig(RouterEnsemblerConfig): def __init__(self, - id: int, experiment_mappings: List[Dict[str, str]]): """ Method to create a new standard ensembler - :param id: id of the ensembler :param experiment_mappings: configured mappings between routes and treatments """ self.experiment_mappings = experiment_mappings - super().__init__(id=id, type="standard") + super().__init__(type="standard") @property def experiment_mappings(self) -> List[Dict[str, str]]: diff --git a/sdk/turing/router/router.py b/sdk/turing/router/router.py index eabb4bbbe..df103a5d9 100644 --- a/sdk/turing/router/router.py +++ b/sdk/turing/router/router.py @@ -33,6 +33,7 @@ def __init__(self, environment_name: str, monitoring_url: str, status: str, + version: int = None, config: Dict = None, endpoint: str = None, **kwargs): @@ -47,8 +48,10 @@ def __init__(self, if config is not None: self._config = RouterConfig(name=name, environment_name=environment_name, **config) + self._version = config.get('version') else: - self._config = None + self._config = config + self._version = version @property def id(self) -> int: @@ -82,6 +85,10 @@ def status(self) -> RouterStatus: def config(self) -> 'RouterConfig': return self._config + @property + def version(self) -> int: + return self._version + @classmethod def list(cls) -> List['Router']: """ @@ -128,7 +135,8 @@ def update(self, config: RouterConfig) -> 'Router': Update the current router with a new set of configs specified in the RouterConfig argument :param config: configuration of router - :return: instance of router updated (self) + :return: instance of router (self); this router contains details of the currently deployed router; it contains + the details of the currently DEPLOYED router version """ self._config = config updated_router = Router.from_open_api( @@ -201,11 +209,13 @@ def get_events(self) -> List[turing.generated.models.Event]: def wait_for_status(self, status: RouterStatus, max_tries: int = 15, duration: float = 10.0): for i in range(1, max_tries + 1): logger.debug(f"Checking if router {self.id} is {status.value}...") - cur_status = Router.get(self.id).status + current_router = Router.get(self.id) + cur_status = current_router.status if cur_status == status: # Wait for backend components to fully resolve time.sleep(5) logger.debug(f"Router {self.id} is finally {status.value}.") + self.__dict__ = current_router.__dict__ return else: logger.debug(f"Router {self.id} is {cur_status.value}.") diff --git a/sdk/turing/session.py b/sdk/turing/session.py index a90252b5c..e85e3ec3b 100644 --- a/sdk/turing/session.py +++ b/sdk/turing/session.py @@ -7,8 +7,8 @@ from turing.generated import ApiClient, Configuration from turing.generated.apis import EnsemblerApi, EnsemblingJobApi, ProjectApi, RouterApi from turing.generated.models import (Project, Ensembler, EnsemblingJob, EnsemblerJobStatus, EnsemblersPaginatedResults, - EnsemblingJobPaginatedResults, IdObject, Router, RouterDetails, RouterConfig, - RouterVersion) + EnsemblingJobPaginatedResults, JobId, RouterId, RouterIdObject, RouterIdAndVersion, + Router, RouterDetails, RouterConfig, RouterVersion) def require_active_project(f): @@ -181,7 +181,7 @@ def get_ensembling_job(self, job_id: int) -> EnsemblingJob: ) @require_active_project - def terminate_ensembling_job(self, job_id: int) -> IdObject: + def terminate_ensembling_job(self, job_id: int) -> JobId: return EnsemblingJobApi(self._api_client).terminate_ensembling_job( project_id=self.active_project.id, job_id=job_id @@ -215,7 +215,7 @@ def create_router(self, router_config: RouterConfig) -> RouterDetails: router_config=router_config) @require_active_project - def delete_router(self, router_id: int) -> IdObject: + def delete_router(self, router_id: int) -> RouterId: """ Delete router given its router ID """ @@ -244,7 +244,7 @@ def update_router(self, router_id: int, router_config: RouterConfig) -> Router: router_config=router_config) @require_active_project - def deploy_router(self, router_id: int) -> IdObject: + def deploy_router(self, router_id: int) -> RouterIdAndVersion: """ Deploy router given its router ID """ @@ -254,7 +254,7 @@ def deploy_router(self, router_id: int) -> IdObject: ) @require_active_project - def undeploy_router(self, router_id: int) -> IdObject: + def undeploy_router(self, router_id: int) -> RouterIdObject: """ Undeploy router given its router ID """ @@ -285,7 +285,7 @@ def get_router_version(self, router_id: int, version: int) -> RouterVersion: ) @require_active_project - def delete_router_version(self, router_id: int, version: int) -> IdObject: + def delete_router_version(self, router_id: int, version: int) -> RouterIdAndVersion: """ Delete specific router version given its router ID and version """ @@ -296,7 +296,7 @@ def delete_router_version(self, router_id: int, version: int) -> IdObject: ) @require_active_project - def deploy_router_version(self, router_id: int, version: int) -> IdObject: + def deploy_router_version(self, router_id: int, version: int) -> RouterIdAndVersion: """ Deploy specific router version by its router ID and version """ @@ -307,7 +307,7 @@ def deploy_router_version(self, router_id: int, version: int) -> IdObject: ) @require_active_project - def get_router_events(self, router_id: int) -> turing.generated.models.InlineResponse2002: + def get_router_events(self, router_id: int) -> turing.generated.models.RouterEvents: """ Fetch deployment events associated with the router with the given router ID """ diff --git a/ui/src/components/form/service_account_combo_box/ServiceAccoutComboBox.js b/ui/src/components/form/service_account_combo_box/ServiceAccountComboBox.js similarity index 100% rename from ui/src/components/form/service_account_combo_box/ServiceAccoutComboBox.js rename to ui/src/components/form/service_account_combo_box/ServiceAccountComboBox.js diff --git a/ui/src/router/components/configuration/RouterConfigDetails.js b/ui/src/router/components/configuration/RouterConfigDetails.js index 58595cde5..787c61047 100644 --- a/ui/src/router/components/configuration/RouterConfigDetails.js +++ b/ui/src/router/components/configuration/RouterConfigDetails.js @@ -29,7 +29,9 @@ export const RouterConfigDetails = ({ projectId, config }) => { { title: "Ensembler", iconType: "aggregate", - children: , + children: ( + + ), }, { title: "Outcome Tracking", diff --git a/ui/src/router/components/configuration/components/EnsemblerConfigSection.js b/ui/src/router/components/configuration/components/EnsemblerConfigSection.js index e913fe7c4..2636fb1c4 100644 --- a/ui/src/router/components/configuration/components/EnsemblerConfigSection.js +++ b/ui/src/router/components/configuration/components/EnsemblerConfigSection.js @@ -3,8 +3,11 @@ import { EuiPanel } from "@elastic/eui"; import { DockerConfigViewGroup } from "./docker_config_section/DockerConfigViewGroup"; import { TreatmentMappingConfigSection } from "./TreatmentMappingConfigSection"; import { ExperimentEngineContextProvider } from "../../../../providers/experiments/ExperimentEngineContextProvider"; +import { PyFuncConfigViewGroup } from "./pyfunc_config_section/PyFuncConfigViewGroup"; +import { EnsemblersContextContextProvider } from "../../../../providers/ensemblers/context"; export const EnsemblerConfigSection = ({ + projectId, config: { ensembler, experiment_engine: { type, config: experimentConfig }, @@ -14,6 +17,17 @@ export const EnsemblerConfigSection = ({ Not Configured ) : ( + {ensembler.type === "pyfunc" && ( + + + + )} {ensembler.type === "docker" && ( { + const items = [ + { + title: "Pyfunc Ensembler Details", + children: , + }, + ]; + + if (!!dockerConfig) { + items.push({ + title: "Container", + children: , + }); + } + + return ( + + + + +
+ {!!dockerConfig ? ( + + + + + + ) : null} +
+
+ ); +}; diff --git a/ui/src/router/components/configuration/components/pyfunc_config_section/PyFuncRefConfigTable.js b/ui/src/router/components/configuration/components/pyfunc_config_section/PyFuncRefConfigTable.js new file mode 100644 index 000000000..70344e46a --- /dev/null +++ b/ui/src/router/components/configuration/components/pyfunc_config_section/PyFuncRefConfigTable.js @@ -0,0 +1,33 @@ +import React, { useContext } from "react"; +import { EuiDescriptionList } from "@elastic/eui"; +import EnsemblersContext from "../../../../../providers/ensemblers/context"; + +export const PyFuncRefConfigTable = ({ config: { ensembler_id } }) => { + const { ensemblers } = useContext(EnsemblersContext); + + const ensembler_name = Object.values(ensemblers) + .filter((value) => value.id === ensembler_id) + .map((ensembler) => ensembler.name); + + const items = [ + { + title: "Ensembler ID", + description: ensembler_id, + }, + { + title: "Ensembler Name", + description: ensembler_name, + }, + ]; + + return ( + + ); +}; diff --git a/ui/src/router/components/form/components/docker_config/DockerDeploymentPanel.js b/ui/src/router/components/form/components/docker_config/DockerDeploymentPanel.js index 74383c99f..1cbf504ae 100644 --- a/ui/src/router/components/form/components/docker_config/DockerDeploymentPanel.js +++ b/ui/src/router/components/form/components/docker_config/DockerDeploymentPanel.js @@ -17,7 +17,7 @@ import { EuiFieldDuration } from "../../../../../components/form/field_duration/ import SecretsContext from "../../../../../providers/secrets/context"; import DockerRegistriesContext from "../../../../../providers/docker/context"; import { useOnChangeHandler } from "../../../../../components/form/hooks/useOnChangeHandler"; -import { ServiceAccountComboBox } from "../../../../../components/form/service_account_combo_box/ServiceAccoutComboBox"; +import { ServiceAccountComboBox } from "../../../../../components/form/service_account_combo_box/ServiceAccountComboBox"; import { useConfig } from "../../../../../config"; const imageOptions = []; diff --git a/ui/src/router/components/form/components/ensembler_config/typeOptions.js b/ui/src/router/components/form/components/ensembler_config/typeOptions.js index c9d19c27a..a73dbc5a3 100644 --- a/ui/src/router/components/form/components/ensembler_config/typeOptions.js +++ b/ui/src/router/components/form/components/ensembler_config/typeOptions.js @@ -32,6 +32,17 @@ const typeOptions = [
), }, + { + value: "pyfunc", + inputDisplay: "Pyfunc", + description: ( + + Turing will build and deploy the selected pyfunc ensembler and will send + to it responses from all routes, together with the treatment + configuration, for ensembling + + ), + }, { value: "external", inputDisplay: "External (Coming Soon)", diff --git a/ui/src/router/components/form/components/outcome_config/bigquery/BigQueryConfigPanel.js b/ui/src/router/components/form/components/outcome_config/bigquery/BigQueryConfigPanel.js index da018a6da..09d49700d 100644 --- a/ui/src/router/components/form/components/outcome_config/bigquery/BigQueryConfigPanel.js +++ b/ui/src/router/components/form/components/outcome_config/bigquery/BigQueryConfigPanel.js @@ -11,7 +11,7 @@ import { import SecretsContext from "../../../../../../providers/secrets/context"; import { FormLabelWithToolTip } from "../../../../../../components/form/label_with_tooltip/FormLabelWithToolTip"; import { useOnChangeHandler } from "../../../../../../components/form/hooks/useOnChangeHandler"; -import { ServiceAccountComboBox } from "../../../../../../components/form/service_account_combo_box/ServiceAccoutComboBox"; +import { ServiceAccountComboBox } from "../../../../../../components/form/service_account_combo_box/ServiceAccountComboBox"; export const BigQueryConfigPanel = ({ projectId, diff --git a/ui/src/router/components/form/components/pyfunc_config/PyFuncConfigFormGroup.js b/ui/src/router/components/form/components/pyfunc_config/PyFuncConfigFormGroup.js new file mode 100644 index 000000000..8317d290b --- /dev/null +++ b/ui/src/router/components/form/components/pyfunc_config/PyFuncConfigFormGroup.js @@ -0,0 +1,57 @@ +import React, { Fragment, useEffect } from "react"; +import { EuiFlexItem } from "@elastic/eui"; +import { useConfig } from "../../../../../config"; +import { ResourcesPanel } from "../ResourcesPanel"; +import { SecretsContextProvider } from "../../../../../providers/secrets/context"; +import { useOnChangeHandler } from "../../../../../components/form/hooks/useOnChangeHandler"; +import { PyFuncEnsembler } from "../../../../../services/ensembler"; +import { PyFuncDeploymentPanel } from "./PyFuncDeploymentPanel"; +import { EnsemblersContextContextProvider } from "../../../../../providers/ensemblers/context"; + +export const PyFuncConfigFormGroup = ({ + projectId, + pyfuncConfig, + onChangeHandler, + errors = {}, +}) => { + const { + appConfig: { + scaling: { maxAllowedReplica }, + }, + } = useConfig(); + const { onChange } = useOnChangeHandler(onChangeHandler); + + useEffect(() => { + !pyfuncConfig && + onChangeHandler(PyFuncEnsembler.newConfig(parseInt(projectId))); + }, [pyfuncConfig, onChangeHandler, projectId]); + + return ( + !!pyfuncConfig && ( + + + + + + + + + + + + + + ) + ); +}; diff --git a/ui/src/router/components/form/components/pyfunc_config/PyFuncDeploymentPanel.js b/ui/src/router/components/form/components/pyfunc_config/PyFuncDeploymentPanel.js new file mode 100644 index 000000000..6cfc5c665 --- /dev/null +++ b/ui/src/router/components/form/components/pyfunc_config/PyFuncDeploymentPanel.js @@ -0,0 +1,87 @@ +import React, { useContext } from "react"; +import { + EuiComboBox, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiSpacer, +} from "@elastic/eui"; +import { Panel } from "../Panel"; +import { EuiFieldDuration } from "../../../../../components/form/field_duration/EuiFieldDuration"; +import { useOnChangeHandler } from "../../../../../components/form/hooks/useOnChangeHandler"; +import { FormLabelWithToolTip } from "../../../../../components/form/label_with_tooltip/FormLabelWithToolTip"; +import EnsemblersContext from "../../../../../providers/ensemblers/context"; + +export const PyFuncDeploymentPanel = ({ + values: { ensembler_id, timeout }, + onChangeHandler, + errors = {}, +}) => { + const { ensemblers } = useContext(EnsemblersContext); + const { onChange } = useOnChangeHandler(onChangeHandler); + + let options = Object.values(ensemblers).reduce((pyfunc_ensemblers, val) => { + pyfunc_ensemblers.push({ + value: val["id"], + label: val["name"], + }); + return pyfunc_ensemblers; + }, []); + + const selectedOption = options.find( + (option) => option.value === ensembler_id + ); + + const onEnsemblerIdChange = (selected) => { + onChange("ensembler_id")(selected[0]?.value); + }; + + return ( + + + + + + } + isInvalid={!!errors.ensembler_id} + error={errors.ensembler_id} + display="row"> + + + + + + + + + + + + + + ); +}; diff --git a/ui/src/router/components/form/steps/EnsemblerStep.js b/ui/src/router/components/form/steps/EnsemblerStep.js index 48c3ea18d..31187e7c6 100644 --- a/ui/src/router/components/form/steps/EnsemblerStep.js +++ b/ui/src/router/components/form/steps/EnsemblerStep.js @@ -8,6 +8,7 @@ import { get } from "../../../../components/form/utils"; import { StandardEnsemblerFormGroup } from "../components/ensembler_config/standard_ensembler/StandardEnsemblerFormGroup"; import { useOnChangeHandler } from "../../../../components/form/hooks/useOnChangeHandler"; import ExperimentEngineContext from "../../../../providers/experiments/context"; +import { PyFuncConfigFormGroup } from "../components/pyfunc_config/PyFuncConfigFormGroup"; export const EnsemblerStep = ({ projectId }) => { const { @@ -45,6 +46,15 @@ export const EnsemblerStep = ({ projectId }) => { /> )} + {ensembler.type === "pyfunc" && ( + + )} + {ensembler.type === "standard" && ( resource_request: resourceRequestSchema(maxAllowedReplica), }); +const pyfuncDeploymentSchema = (maxAllowedReplica) => + yup.object().shape({ + project_id: yup.number().integer().required("Project ID is required"), + ensembler_id: yup.number().integer().required("Ensembler ID is required"), + timeout: timeoutSchema.required("Timeout is required"), + resource_request: resourceRequestSchema(maxAllowedReplica), + }); + const mappingSchema = yup.object().shape({ experiment: yup.string().required("Experiment name is required"), treatment: yup.string().required("Treatment name is required"), @@ -290,7 +298,7 @@ const schema = (maxAllowedReplica) => [ .mixed() .required("Valid Ensembler type should be selected") .oneOf( - ["nop", "docker", "standard"], + ["nop", "docker", "standard", "pyfunc"], "Valid Ensembler type should be selected" ), docker_config: yup.mixed().when("type", { @@ -301,6 +309,10 @@ const schema = (maxAllowedReplica) => [ is: "standard", then: standardEnsemblerConfigSchema, }), + pyfunc_config: yup.mixed().when("type", { + is: "pyfunc", + then: pyfuncDeploymentSchema(maxAllowedReplica), + }), }), }), }), diff --git a/ui/src/services/ensembler/Ensembler.js b/ui/src/services/ensembler/Ensembler.js index cd97d8e7d..96ec83be3 100644 --- a/ui/src/services/ensembler/Ensembler.js +++ b/ui/src/services/ensembler/Ensembler.js @@ -1,4 +1,9 @@ -import { DockerEnsembler, NopEnsembler, StandardEnsembler } from "./index"; +import { + DockerEnsembler, + NopEnsembler, + StandardEnsembler, + PyFuncEnsembler, +} from "./index"; export class Ensembler { constructor(type) { @@ -12,6 +17,8 @@ export class Ensembler { return DockerEnsembler.fromJson(json); case "standard": return StandardEnsembler.fromJson(json); + case "pyfunc": + return PyFuncEnsembler.fromJson(json); default: return new NopEnsembler(); } @@ -23,6 +30,8 @@ export class Ensembler { return { type: this.type, docker_config: this.docker_config }; case "standard": return { type: this.type, standard_config: this.standard_config }; + case "pyfunc": + return { type: this.type, pyfunc_config: this.pyfunc_config }; default: return { ...this }; } diff --git a/ui/src/services/ensembler/PyFuncEnsembler.js b/ui/src/services/ensembler/PyFuncEnsembler.js new file mode 100644 index 000000000..4cc64c698 --- /dev/null +++ b/ui/src/services/ensembler/PyFuncEnsembler.js @@ -0,0 +1,31 @@ +import { Ensembler } from "./Ensembler"; + +const objectAssignDeep = require(`object-assign-deep`); + +export class PyFuncEnsembler extends Ensembler { + constructor() { + super("pyfunc"); + this.pyfunc_config = PyFuncEnsembler.newConfig(); + + this.toJSON = this.toJSON.bind(this); + } + + static fromJson(json = {}) { + const ensembler = new PyFuncEnsembler(); + ensembler.pyfunc_config = objectAssignDeep({}, json.pyfunc_config); + return ensembler; + } + + static newConfig(project_id) { + return { + project_id: project_id, + resource_request: { + cpu_request: "500m", + memory_request: "512Mi", + min_replica: 0, + max_replica: 2, + }, + timeout: "60ms", + }; + } +} diff --git a/ui/src/services/ensembler/index.js b/ui/src/services/ensembler/index.js index ca2b8d068..9eb39c65b 100644 --- a/ui/src/services/ensembler/index.js +++ b/ui/src/services/ensembler/index.js @@ -2,3 +2,4 @@ export * from "./Ensembler"; export * from "./DockerEnsembler"; export * from "./NopEnsembler"; export * from "./StandardEnsembler"; +export * from "./PyFuncEnsembler"; diff --git a/ui/src/services/version/RouterVersion.js b/ui/src/services/version/RouterVersion.js index f34a3b94e..1805716a3 100644 --- a/ui/src/services/version/RouterVersion.js +++ b/ui/src/services/version/RouterVersion.js @@ -87,6 +87,10 @@ export class RouterVersion { ? { docker_config: this.ensembler.docker_config, } + : this.ensembler.type === "pyfunc" + ? { + pyfunc_config: this.ensembler.pyfunc_config, + } : undefined), } : {