You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@submarine.apache.org by pi...@apache.org on 2022/01/25 04:35:26 UTC
[submarine] branch master updated: SUBMARINE-1157. Serving model via Python API or CLI
This is an automated email from the ASF dual-hosted git repository.
pingsutw pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/submarine.git
The following commit(s) were added to refs/heads/master by this push:
new b8284c0 SUBMARINE-1157. Serving model via Python API or CLI
b8284c0 is described below
commit b8284c00d71a4c875b0975de24d74f00bb0aa181
Author: KUAN-HSUN-LI <b0...@ntu.edu.tw>
AuthorDate: Wed Jan 12 21:13:01 2022 +0800
SUBMARINE-1157. Serving model via Python API or CLI
### What is this PR for?
Create the Python API and CLI command for serving a registered model.
### What type of PR is it?
[Feature]
### Todos
* [x] - Save Pytorch model via torchscript
* [x] - Generate the Serve API
* [x] - Transfer the model description file to the config which is used in the Triton Inference Server. (It will be handled in this issue https://issues.apache.org/jira/browse/SUBMARINE-1173)
### What is the Jira issue?
https://issues.apache.org/jira/browse/SUBMARINE-1157
### How should this be tested?
The tests will provide in another PR because the transfer of the config is not finished.
### Screenshots (if appropriate)
https://user-images.githubusercontent.com/38066413/147928471-7e8cc70b-99df-4b49-89bb-bc1627bbb885.mp4
### Questions:
* Do the license files need updating? No
* Are there breaking changes for older versions? No
* Does this need new documentation? Yes
Author: KUAN-HSUN-LI <b0...@ntu.edu.tw>
Signed-off-by: Kevin <pi...@apache.org>
Closes #860 from KUAN-HSUN-LI/SUBMARINE-1157 and squashes the following commits:
06889e1e [KUAN-HSUN-LI] SUBMARINE-1157. fix
7ab6b043 [KUAN-HSUN-LI] SUBMARINE-1157. fix
04401830 [KUAN-HSUN-LI] SUBMARINE-1157. Python API for searving
99bfd889 [KUAN-HSUN-LI] SUBMARINE-1157. Python API for searving
fb61ade8 [KUAN-HSUN-LI] SUBMARINE-1157. Generate the serve API
---
dev-support/pysubmarine/openapi.json | 996 +++++++++++++++++++++
submarine-sdk/pysubmarine/setup.py | 2 +-
submarine-sdk/pysubmarine/submarine/cli/main.py | 11 +
.../pysubmarine/submarine/cli/serve/__init__.py | 25 +
.../pysubmarine/submarine/cli/serve/command.py | 128 +++
.../pysubmarine/submarine/client/__init__.py | 2 +
.../pysubmarine/submarine/client/api/__init__.py | 1 +
.../submarine/client/api/experiment_client.py | 20 +-
.../pysubmarine/submarine/client/api/serve_api.py | 360 ++++++++
.../submarine/client/api/serve_client.py | 53 ++
.../submarine/client/models/__init__.py | 1 +
.../submarine/client/models/serve_spec.py | 223 +++++
.../client/{api/__init__.py => utils/api_utils.py} | 27 +-
.../pysubmarine/submarine/models/pytorch.py | 6 +-
.../pysubmarine/submarine/tracking/client.py | 22 +-
.../pysubmarine/submarine/tracking/fluent.py | 19 +
.../org/apache/submarine/server/Bootstrap.java | 3 +-
17 files changed, 1870 insertions(+), 29 deletions(-)
diff --git a/dev-support/pysubmarine/openapi.json b/dev-support/pysubmarine/openapi.json
new file mode 100644
index 0000000..0c25917
--- /dev/null
+++ b/dev-support/pysubmarine/openapi.json
@@ -0,0 +1,996 @@
+{
+ "openapi" : "3.0.1",
+ "info" : {
+ "title" : "Submarine API",
+ "description" : "The Submarine REST API allows you to access Submarine resources such as, \nexperiments, environments and notebooks. The \nAPI is hosted under the /v1 path on the Submarine server. For example, \nto list experiments on a server hosted at http://localhost:8080, access\nhttp://localhost:8080/api/v1/experiment/",
+ "termsOfService" : "http://swagger.io/terms/",
+ "contact" : {
+ "email" : "dev@submarine.apache.org"
+ },
+ "license" : {
+ "name" : "Apache 2.0",
+ "url" : "http://www.apache.org/licenses/LICENSE-2.0.html"
+ },
+ "version" : "0.7.0-SNAPSHOT"
+ },
+ "servers" : [ {
+ "url" : "/api"
+ } ],
+ "paths" : {
+ "/v1/environment/{id}" : {
+ "get" : {
+ "tags" : [ "environment" ],
+ "summary" : "Find environment by name",
+ "operationId" : "getEnvironment",
+ "parameters" : [ {
+ "name" : "id",
+ "in" : "path",
+ "required" : true,
+ "schema" : {
+ "type" : "string"
+ }
+ } ],
+ "responses" : {
+ "default" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/Environment"
+ }
+ }
+ }
+ },
+ "404" : {
+ "description" : "Environment not found"
+ }
+ }
+ },
+ "delete" : {
+ "tags" : [ "environments" ],
+ "summary" : "Delete the environment",
+ "operationId" : "deleteEnvironment",
+ "parameters" : [ {
+ "name" : "id",
+ "in" : "path",
+ "required" : true,
+ "schema" : {
+ "type" : "string"
+ }
+ } ],
+ "responses" : {
+ "default" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/Environment"
+ }
+ }
+ }
+ },
+ "404" : {
+ "description" : "Environment not found"
+ }
+ }
+ },
+ "patch" : {
+ "tags" : [ "environments" ],
+ "summary" : "Update the environment with job spec",
+ "operationId" : "updateEnvironment",
+ "parameters" : [ {
+ "name" : "id",
+ "in" : "path",
+ "required" : true,
+ "schema" : {
+ "type" : "string"
+ }
+ } ],
+ "requestBody" : {
+ "content" : {
+ "application/yaml" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/EnvironmentSpec"
+ }
+ },
+ "application/json" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/EnvironmentSpec"
+ }
+ }
+ }
+ },
+ "responses" : {
+ "default" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/Environment"
+ }
+ }
+ }
+ },
+ "404" : {
+ "description" : "Environment not found"
+ }
+ }
+ }
+ },
+ "/v1/environment" : {
+ "get" : {
+ "tags" : [ "environments" ],
+ "summary" : "List of Environments",
+ "operationId" : "listEnvironment",
+ "parameters" : [ {
+ "name" : "status",
+ "in" : "query",
+ "schema" : {
+ "type" : "string"
+ }
+ } ],
+ "responses" : {
+ "default" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/Environment"
+ }
+ }
+ }
+ }
+ }
+ },
+ "post" : {
+ "tags" : [ "environment" ],
+ "summary" : "Create a environment",
+ "operationId" : "createEnvironment",
+ "requestBody" : {
+ "content" : {
+ "application/yaml" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/EnvironmentSpec"
+ }
+ },
+ "application/json" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/EnvironmentSpec"
+ }
+ }
+ }
+ },
+ "responses" : {
+ "default" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/Environment"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/v1/experiment/logs/{id}" : {
+ "get" : {
+ "tags" : [ "experiment" ],
+ "summary" : "Log experiment by id",
+ "operationId" : "getLog",
+ "parameters" : [ {
+ "name" : "id",
+ "in" : "path",
+ "required" : true,
+ "schema" : {
+ "type" : "string"
+ }
+ } ],
+ "responses" : {
+ "default" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/JsonResponse"
+ }
+ }
+ }
+ },
+ "404" : {
+ "description" : "Experiment not found"
+ }
+ }
+ }
+ },
+ "/v1/experiment/ping" : {
+ "get" : {
+ "tags" : [ "experiment" ],
+ "summary" : "Ping submarine server",
+ "description" : "Return the Pong message for test the connectivity",
+ "operationId" : "ping",
+ "responses" : {
+ "200" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "type" : "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/v1/experiment" : {
+ "get" : {
+ "tags" : [ "experiment" ],
+ "summary" : "List experiments",
+ "operationId" : "listExperiments",
+ "parameters" : [ {
+ "name" : "status",
+ "in" : "query",
+ "schema" : {
+ "type" : "string"
+ }
+ } ],
+ "responses" : {
+ "default" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/JsonResponse"
+ }
+ }
+ }
+ }
+ }
+ },
+ "post" : {
+ "tags" : [ "experiment" ],
+ "summary" : "Create an experiment",
+ "operationId" : "createExperiment",
+ "requestBody" : {
+ "content" : {
+ "application/yaml" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/ExperimentSpec"
+ }
+ },
+ "application/json" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/ExperimentSpec"
+ }
+ }
+ }
+ },
+ "responses" : {
+ "default" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/JsonResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/v1/experiment/{name}" : {
+ "post" : {
+ "tags" : [ "experiment" ],
+ "summary" : "use experiment template to create an experiment",
+ "operationId" : "SubmitExperimentTemplate",
+ "parameters" : [ {
+ "name" : "name",
+ "in" : "path",
+ "required" : true,
+ "schema" : {
+ "type" : "string"
+ }
+ } ],
+ "requestBody" : {
+ "content" : {
+ "application/yaml" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/ExperimentTemplateSubmit"
+ }
+ },
+ "application/json" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/ExperimentTemplateSubmit"
+ }
+ }
+ }
+ },
+ "responses" : {
+ "default" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/JsonResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/v1/experiment/{id}" : {
+ "get" : {
+ "tags" : [ "experiment" ],
+ "summary" : "Get the experiment's detailed info by id",
+ "operationId" : "getExperiment",
+ "parameters" : [ {
+ "name" : "id",
+ "in" : "path",
+ "required" : true,
+ "schema" : {
+ "type" : "string"
+ }
+ } ],
+ "responses" : {
+ "default" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/JsonResponse"
+ }
+ }
+ }
+ },
+ "404" : {
+ "description" : "Experiment not found"
+ }
+ }
+ },
+ "delete" : {
+ "tags" : [ "experiment" ],
+ "summary" : "Delete the experiment",
+ "operationId" : "deleteExperiment",
+ "parameters" : [ {
+ "name" : "id",
+ "in" : "path",
+ "required" : true,
+ "schema" : {
+ "type" : "string"
+ }
+ } ],
+ "responses" : {
+ "default" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/JsonResponse"
+ }
+ }
+ }
+ },
+ "404" : {
+ "description" : "Experiment not found"
+ }
+ }
+ },
+ "patch" : {
+ "tags" : [ "experiment" ],
+ "summary" : "Update the experiment in the submarine server with spec",
+ "operationId" : "patchExperiment",
+ "parameters" : [ {
+ "name" : "id",
+ "in" : "path",
+ "required" : true,
+ "schema" : {
+ "type" : "string"
+ }
+ } ],
+ "requestBody" : {
+ "content" : {
+ "application/yaml" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/ExperimentSpec"
+ }
+ },
+ "application/json" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/ExperimentSpec"
+ }
+ }
+ }
+ },
+ "responses" : {
+ "default" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/JsonResponse"
+ }
+ }
+ }
+ },
+ "404" : {
+ "description" : "Experiment not found"
+ }
+ }
+ }
+ },
+ "/v1/experiment/tensorboard" : {
+ "get" : {
+ "tags" : [ "experiment" ],
+ "summary" : "Get tensorboard's information",
+ "operationId" : "getTensorboardInfo",
+ "responses" : {
+ "default" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/JsonResponse"
+ }
+ }
+ }
+ },
+ "404" : {
+ "description" : "Tensorboard not found"
+ }
+ }
+ }
+ },
+ "/v1/experiment/mlflow" : {
+ "get" : {
+ "tags" : [ "experiment" ],
+ "summary" : "Get mlflow's information",
+ "operationId" : "getMLflowInfo",
+ "responses" : {
+ "default" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/JsonResponse"
+ }
+ }
+ }
+ },
+ "404" : {
+ "description" : "MLflow not found"
+ }
+ }
+ }
+ },
+ "/v1/experiment/logs" : {
+ "get" : {
+ "tags" : [ "experiment" ],
+ "summary" : "List experiment's log",
+ "operationId" : "listLog",
+ "parameters" : [ {
+ "name" : "status",
+ "in" : "query",
+ "schema" : {
+ "type" : "string"
+ }
+ } ],
+ "responses" : {
+ "default" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/JsonResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/v1/notebook/ping" : {
+ "get" : {
+ "tags" : [ "notebook" ],
+ "summary" : "Ping submarine server",
+ "description" : "Return the Pong message for test the connectivity",
+ "operationId" : "ping_1",
+ "responses" : {
+ "200" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "type" : "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/v1/notebook" : {
+ "get" : {
+ "tags" : [ "notebook" ],
+ "summary" : "List notebooks",
+ "operationId" : "listNotebooks",
+ "parameters" : [ {
+ "name" : "id",
+ "in" : "query",
+ "schema" : {
+ "type" : "string"
+ }
+ } ],
+ "responses" : {
+ "default" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/JsonResponse"
+ }
+ }
+ }
+ }
+ }
+ },
+ "post" : {
+ "tags" : [ "notebook" ],
+ "summary" : "Create a notebook instance",
+ "operationId" : "createNotebook",
+ "requestBody" : {
+ "content" : {
+ "application/yaml" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/NotebookSpec"
+ }
+ },
+ "application/json" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/NotebookSpec"
+ }
+ }
+ }
+ },
+ "responses" : {
+ "default" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/JsonResponse"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/v1/notebook/{id}" : {
+ "get" : {
+ "tags" : [ "notebook" ],
+ "summary" : "Get detailed info about the notebook",
+ "operationId" : "getNotebook",
+ "parameters" : [ {
+ "name" : "id",
+ "in" : "path",
+ "required" : true,
+ "schema" : {
+ "type" : "string"
+ }
+ } ],
+ "responses" : {
+ "default" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/JsonResponse"
+ }
+ }
+ }
+ },
+ "404" : {
+ "description" : "Notebook not found"
+ }
+ }
+ },
+ "delete" : {
+ "tags" : [ "notebook" ],
+ "summary" : "Delete the notebook",
+ "operationId" : "deleteNotebook",
+ "parameters" : [ {
+ "name" : "id",
+ "in" : "path",
+ "required" : true,
+ "schema" : {
+ "type" : "string"
+ }
+ } ],
+ "responses" : {
+ "default" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/JsonResponse"
+ }
+ }
+ }
+ },
+ "404" : {
+ "description" : "Notebook not found"
+ }
+ }
+ }
+ },
+ "/v1/serve/ping" : {
+ "get" : {
+ "tags" : [ "serve" ],
+ "summary" : "Ping submarine server",
+ "description" : "Return the Pong message for test the connectivity",
+ "operationId" : "ping_2",
+ "responses" : {
+ "200" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "type" : "string"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/v1/serve" : {
+ "post" : {
+ "tags" : [ "serve" ],
+ "summary" : "Create a serve instance",
+ "operationId" : "createServe",
+ "requestBody" : {
+ "content" : {
+ "application/yaml" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/ServeSpec"
+ }
+ },
+ "application/json" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/ServeSpec"
+ }
+ }
+ }
+ },
+ "responses" : {
+ "default" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/JsonResponse"
+ }
+ }
+ }
+ }
+ }
+ },
+ "delete" : {
+ "tags" : [ "serve" ],
+ "summary" : "Delete the serve instance.",
+ "operationId" : "deleteServe",
+ "requestBody" : {
+ "content" : {
+ "*/*" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/ServeSpec"
+ }
+ }
+ }
+ },
+ "responses" : {
+ "default" : {
+ "description" : "successful operation",
+ "content" : {
+ "application/json; charset=utf-8" : {
+ "schema" : {
+ "$ref" : "#/components/schemas/JsonResponse"
+ }
+ }
+ }
+ },
+ "404" : {
+ "description" : "Serve not found."
+ }
+ }
+ }
+ }
+ },
+ "components" : {
+ "schemas" : {
+ "Environment" : {
+ "type" : "object",
+ "properties" : {
+ "environmentId" : {
+ "$ref" : "#/components/schemas/EnvironmentId"
+ },
+ "environmentSpec" : {
+ "$ref" : "#/components/schemas/EnvironmentSpec"
+ }
+ }
+ },
+ "EnvironmentId" : {
+ "type" : "object",
+ "properties" : {
+ "id" : {
+ "type" : "integer",
+ "format" : "int32"
+ },
+ "serverTimestamp" : {
+ "type" : "integer",
+ "format" : "int64"
+ }
+ }
+ },
+ "EnvironmentSpec" : {
+ "type" : "object",
+ "properties" : {
+ "name" : {
+ "type" : "string"
+ },
+ "dockerImage" : {
+ "type" : "string"
+ },
+ "kernelSpec" : {
+ "$ref" : "#/components/schemas/KernelSpec"
+ },
+ "description" : {
+ "type" : "string"
+ },
+ "image" : {
+ "type" : "string"
+ }
+ }
+ },
+ "KernelSpec" : {
+ "type" : "object",
+ "properties" : {
+ "name" : {
+ "type" : "string"
+ },
+ "channels" : {
+ "type" : "array",
+ "items" : {
+ "type" : "string"
+ }
+ },
+ "condaDependencies" : {
+ "type" : "array",
+ "items" : {
+ "type" : "string"
+ }
+ },
+ "pipDependencies" : {
+ "type" : "array",
+ "items" : {
+ "type" : "string"
+ }
+ }
+ }
+ },
+ "JsonResponse" : {
+ "type" : "object",
+ "properties" : {
+ "code" : {
+ "type" : "integer",
+ "format" : "int32"
+ },
+ "success" : {
+ "type" : "boolean"
+ },
+ "result" : {
+ "type" : "object"
+ },
+ "attributes" : {
+ "type" : "object",
+ "additionalProperties" : {
+ "type" : "object"
+ }
+ }
+ }
+ },
+ "CodeSpec" : {
+ "type" : "object",
+ "properties" : {
+ "syncMode" : {
+ "type" : "string"
+ },
+ "url" : {
+ "type" : "string"
+ }
+ }
+ },
+ "ExperimentMeta" : {
+ "type" : "object",
+ "properties" : {
+ "experimentId" : {
+ "type" : "string"
+ },
+ "name" : {
+ "type" : "string"
+ },
+ "namespace" : {
+ "type" : "string"
+ },
+ "framework" : {
+ "type" : "string"
+ },
+ "cmd" : {
+ "type" : "string"
+ },
+ "envVars" : {
+ "type" : "object",
+ "additionalProperties" : {
+ "type" : "string"
+ }
+ },
+ "tags" : {
+ "type" : "array",
+ "items" : {
+ "type" : "string"
+ }
+ }
+ }
+ },
+ "ExperimentSpec" : {
+ "type" : "object",
+ "properties" : {
+ "meta" : {
+ "$ref" : "#/components/schemas/ExperimentMeta"
+ },
+ "environment" : {
+ "$ref" : "#/components/schemas/EnvironmentSpec"
+ },
+ "spec" : {
+ "type" : "object",
+ "additionalProperties" : {
+ "$ref" : "#/components/schemas/ExperimentTaskSpec"
+ }
+ },
+ "code" : {
+ "$ref" : "#/components/schemas/CodeSpec"
+ }
+ }
+ },
+ "ExperimentTaskSpec" : {
+ "type" : "object",
+ "properties" : {
+ "replicas" : {
+ "type" : "integer",
+ "format" : "int32"
+ },
+ "resources" : {
+ "type" : "string"
+ },
+ "name" : {
+ "type" : "string"
+ },
+ "image" : {
+ "type" : "string"
+ },
+ "cmd" : {
+ "type" : "string"
+ },
+ "envVars" : {
+ "type" : "object",
+ "additionalProperties" : {
+ "type" : "string"
+ }
+ },
+ "cpu" : {
+ "type" : "string"
+ },
+ "memory" : {
+ "type" : "string"
+ },
+ "gpu" : {
+ "type" : "string"
+ }
+ }
+ },
+ "ExperimentTemplateSubmit" : {
+ "type" : "object",
+ "properties" : {
+ "name" : {
+ "type" : "string"
+ },
+ "params" : {
+ "type" : "object",
+ "additionalProperties" : {
+ "type" : "string"
+ }
+ }
+ }
+ },
+ "NotebookMeta" : {
+ "type" : "object",
+ "properties" : {
+ "name" : {
+ "type" : "string"
+ },
+ "namespace" : {
+ "type" : "string"
+ },
+ "ownerId" : {
+ "type" : "string"
+ },
+ "labels" : {
+ "type" : "object",
+ "additionalProperties" : {
+ "type" : "string"
+ }
+ }
+ }
+ },
+ "NotebookPodSpec" : {
+ "type" : "object",
+ "properties" : {
+ "envVars" : {
+ "type" : "object",
+ "additionalProperties" : {
+ "type" : "string"
+ }
+ },
+ "resources" : {
+ "type" : "string"
+ },
+ "cpu" : {
+ "type" : "string"
+ },
+ "memory" : {
+ "type" : "string"
+ },
+ "gpu" : {
+ "type" : "string"
+ }
+ }
+ },
+ "NotebookSpec" : {
+ "type" : "object",
+ "properties" : {
+ "meta" : {
+ "$ref" : "#/components/schemas/NotebookMeta"
+ },
+ "environment" : {
+ "$ref" : "#/components/schemas/EnvironmentSpec"
+ },
+ "spec" : {
+ "$ref" : "#/components/schemas/NotebookPodSpec"
+ }
+ }
+ },
+ "ServeSpec" : {
+ "type" : "object",
+ "properties" : {
+ "modelName" : {
+ "type" : "string"
+ },
+ "modelVersion" : {
+ "type" : "integer",
+ "format" : "int32"
+ },
+ "modelType" : {
+ "type" : "string"
+ },
+ "modelURI" : {
+ "type" : "string"
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/submarine-sdk/pysubmarine/setup.py b/submarine-sdk/pysubmarine/setup.py
index 18abee4..bef6a7e 100644
--- a/submarine-sdk/pysubmarine/setup.py
+++ b/submarine-sdk/pysubmarine/setup.py
@@ -38,7 +38,7 @@ setup(
"urllib3>=1.15.1",
"certifi>=14.05.14",
"python-dateutil>=2.5.3",
- "pyarrow==0.17.0",
+ "pyarrow>=6.0.1",
"boto3>=1.17.58",
"click==8.0.3",
"rich==10.15.2",
diff --git a/submarine-sdk/pysubmarine/submarine/cli/main.py b/submarine-sdk/pysubmarine/submarine/cli/main.py
index 2998f9a..3815f3a 100644
--- a/submarine-sdk/pysubmarine/submarine/cli/main.py
+++ b/submarine-sdk/pysubmarine/submarine/cli/main.py
@@ -22,6 +22,7 @@ from submarine.cli.environment import command as environment_cmd
from submarine.cli.experiment import command as experiment_cmd
from submarine.cli.notebook import command as notebook_cmd
from submarine.cli.sandbox import command as sandbox_cmd
+from submarine.cli.serve import command as serve_cmd
@click.group()
@@ -40,6 +41,11 @@ def cmdgrp_get():
pass
+@entry_point.group("create")
+def cmdgrp_create():
+ pass
+
+
@entry_point.group("delete")
def cmdgrp_delete():
pass
@@ -67,6 +73,11 @@ cmdgrp_delete.add_command(notebook_cmd.delete_notebook)
cmdgrp_list.add_command(environment_cmd.list_environment)
cmdgrp_get.add_command(environment_cmd.get_environment)
cmdgrp_delete.add_command(environment_cmd.delete_environment)
+# # serve
+cmdgrp_list.add_command(serve_cmd.list_serve)
+cmdgrp_get.add_command(serve_cmd.get_serve)
+cmdgrp_create.add_command(serve_cmd.create_serve)
+cmdgrp_delete.add_command(serve_cmd.delete_serve)
# sandbox
cmdgrp_sandbox.add_command(sandbox_cmd.start_sandbox)
diff --git a/submarine-sdk/pysubmarine/submarine/cli/serve/__init__.py b/submarine-sdk/pysubmarine/submarine/cli/serve/__init__.py
new file mode 100644
index 0000000..95edd66
--- /dev/null
+++ b/submarine-sdk/pysubmarine/submarine/cli/serve/__init__.py
@@ -0,0 +1,25 @@
+"""
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+"""
+
+from submarine.cli.serve.command import create_serve, delete_serve, get_serve, list_serve
+
+__all__ = [
+ "list_serve",
+ "get_serve",
+ "create_serve",
+ "delete_serve",
+]
diff --git a/submarine-sdk/pysubmarine/submarine/cli/serve/command.py b/submarine-sdk/pysubmarine/submarine/cli/serve/command.py
new file mode 100644
index 0000000..066e949
--- /dev/null
+++ b/submarine-sdk/pysubmarine/submarine/cli/serve/command.py
@@ -0,0 +1,128 @@
+"""
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+ http://www.apache.org/licenses/LICENSE-2.0
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+"""
+import json
+import time
+
+import click
+from rich.console import Console
+from rich.json import JSON as richJSON
+from rich.panel import Panel
+
+from submarine.cli.config.config import loadConfig
+from submarine.client.api.serve_client import ServeClient
+from submarine.client.api_client import ApiException
+
+submarineCliConfig = loadConfig()
+serveClient = ServeClient(
+ f"http://{submarineCliConfig.connection.hostname}:{submarineCliConfig.connection.port}"
+ if submarineCliConfig
+ else "http://localhost:32080"
+)
+
+POLLING_INTERVAL = 1 # sec
+TIMEOUT = 30 # sec
+
+
+@click.command("serve")
+def list_serve():
+ """List serves"""
+ click.echo("The command is not supported yet.") # TODO(kuanhsun)
+ click.echo("list serve!")
+
+
+@click.command("serve")
+@click.argument("model_name")
+@click.argument("model_version", type=int)
+def get_serve(model_name: str, model_version: int):
+ """Get serve"""
+ click.echo("The command is not supported yet.") # TODO(kuanhsun)
+ click.echo(f"get serve! model name: {model_name}, model version: {model_version}")
+
+
+@click.command("serve")
+@click.argument("model_name")
+@click.argument("model_version", type=int)
+def create_serve(model_name: str, model_version: int):
+ """Create serve"""
+ console = Console()
+ try:
+ thread = serveClient.create_serve(model_name, model_version, async_req=True)
+ timeout = time.time() + TIMEOUT
+ with console.status(
+ f"[bold green] Creating Serve with name: {model_name}, version: {model_version}"
+ ):
+ while not thread.ready():
+ time.sleep(POLLING_INTERVAL)
+ if time.time() > timeout:
+ console.print("[bold red] Timeout!")
+ return
+ result = thread.get()
+ click.echo(result)
+
+ except ApiException as err:
+ if err.body is not None:
+ errbody = json.loads(err.body)
+ click.echo(f"[Api Error] {errbody['message']}")
+ else:
+ click.echo(f"[Api Error] {err}")
+
+
+@click.command("serve")
+@click.argument("model_name")
+@click.argument("model_version", type=int)
+@click.option("--wait", is_flag=True, default=False)
+def delete_serve(model_name: str, model_version: int, wait: bool):
+ """Delete serve"""
+ console = Console()
+ try:
+ thread = serveClient.delete_serve(model_name, model_version, async_req=True)
+ timeout = time.time() + TIMEOUT
+ with console.status(
+ f"[bold green] Deleting Serve with name: {model_name}, version: {model_version}"
+ ):
+ while not thread.ready():
+ time.sleep(POLLING_INTERVAL)
+ if time.time() > timeout:
+ console.print("[bold red] Timeout!")
+ return
+
+ result = thread.get()
+ click.echo(result)
+
+ if wait:
+ if result["status"] == "Deleted":
+ console.print(
+ f"[bold green] Serve: model name:{model_name}, version: {model_version}"
+ )
+ else:
+ console.print("[bold red] Failed")
+ json_data = richJSON.from_data(result)
+ console.print(
+ Panel(
+ json_data,
+ title=(
+ f"[bold green] Serve Deleted: model name:{model_name}, version:"
+ f" {model_version}"
+ ),
+ )
+ )
+ except ApiException as err:
+ if err.body is not None:
+ errbody = json.loads(err.body)
+ click.echo(f"[Api Error] {errbody['message']}")
+ else:
+ click.echo(f"[Api Error] {err}")
diff --git a/submarine-sdk/pysubmarine/submarine/client/__init__.py b/submarine-sdk/pysubmarine/submarine/client/__init__.py
index 47834d0..bec3f2d 100644
--- a/submarine-sdk/pysubmarine/submarine/client/__init__.py
+++ b/submarine-sdk/pysubmarine/submarine/client/__init__.py
@@ -36,6 +36,7 @@ __version__ = "0.7.0-SNAPSHOT"
from submarine.client.api.environment_api import EnvironmentApi
from submarine.client.api.experiment_api import ExperimentApi
from submarine.client.api.notebook_api import NotebookApi
+from submarine.client.api.serve_api import ServeApi
# import ApiClient
from submarine.client.api_client import ApiClient
@@ -60,3 +61,4 @@ from submarine.client.models.kernel_spec import KernelSpec
from submarine.client.models.notebook_meta import NotebookMeta
from submarine.client.models.notebook_pod_spec import NotebookPodSpec
from submarine.client.models.notebook_spec import NotebookSpec
+from submarine.client.models.serve_spec import ServeSpec
diff --git a/submarine-sdk/pysubmarine/submarine/client/api/__init__.py b/submarine-sdk/pysubmarine/submarine/client/api/__init__.py
index 19defd6..741c497 100644
--- a/submarine-sdk/pysubmarine/submarine/client/api/__init__.py
+++ b/submarine-sdk/pysubmarine/submarine/client/api/__init__.py
@@ -19,5 +19,6 @@ from __future__ import absolute_import
from submarine.client.api.environment_api import EnvironmentApi
from submarine.client.api.experiment_api import ExperimentApi
from submarine.client.api.notebook_api import NotebookApi
+from submarine.client.api.serve_api import ServeApi
# flake8: noqa
diff --git a/submarine-sdk/pysubmarine/submarine/client/api/experiment_client.py b/submarine-sdk/pysubmarine/submarine/client/api/experiment_client.py
index 210b1df..1d6afb2 100644
--- a/submarine-sdk/pysubmarine/submarine/client/api/experiment_client.py
+++ b/submarine-sdk/pysubmarine/submarine/client/api/experiment_client.py
@@ -14,29 +14,16 @@
# limitations under the License.
import logging
-import os
import time
from submarine.client.api.experiment_api import ExperimentApi
-from submarine.client.api_client import ApiClient
-from submarine.client.configuration import Configuration
+from submarine.client.utils.api_utils import generate_host, get_api_client
logger = logging.getLogger(__name__)
logging.basicConfig(format="%(message)s")
logging.getLogger().setLevel(logging.INFO)
-def generate_host():
- """
- Generate submarine host
- :return: submarine host
- """
- submarine_server_dns_name = str(os.environ.get("SUBMARINE_SERVER_DNS_NAME"))
- submarine_server_port = str(os.environ.get("SUBMARINE_SERVER_PORT"))
- host = "http://" + submarine_server_dns_name + ":" + submarine_server_port
- return host
-
-
class ExperimentClient:
def __init__(self, host: str = generate_host()):
"""
@@ -44,10 +31,7 @@ class ExperimentClient:
:param host: An HTTP URI like http://submarine-server:8080.
"""
# TODO(pingsutw): support authentication for talking to the submarine server
- self.host = host
- configuration = Configuration()
- configuration.host = host + "/api"
- api_client = ApiClient(configuration=configuration)
+ api_client = get_api_client(host)
self.experiment_api = ExperimentApi(api_client=api_client)
def create_experiment(self, experiment_spec):
diff --git a/submarine-sdk/pysubmarine/submarine/client/api/serve_api.py b/submarine-sdk/pysubmarine/submarine/client/api/serve_api.py
new file mode 100644
index 0000000..803d060
--- /dev/null
+++ b/submarine-sdk/pysubmarine/submarine/client/api/serve_api.py
@@ -0,0 +1,360 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# coding: utf-8
+
+"""
+ Submarine API
+
+ The Submarine REST API allows you to access Submarine resources such as, experiments, environments and notebooks. The API is hosted under the /v1 path on the Submarine server. For example, to list experiments on a server hosted at http://localhost:8080, access http://localhost:8080/api/v1/experiment/ # noqa: E501
+
+ The version of the OpenAPI document: 0.7.0-SNAPSHOT
+ Contact: dev@submarine.apache.org
+ Generated by: https://openapi-generator.tech
+"""
+
+
+from __future__ import absolute_import
+
+import re # noqa: F401
+
+# python 2 and python 3 compatibility library
+import six
+
+from submarine.client.api_client import ApiClient
+from submarine.client.exceptions import ApiTypeError, ApiValueError # noqa: F401
+
+
+class ServeApi(object):
+ """NOTE: This class is auto generated by OpenAPI Generator
+ Ref: https://openapi-generator.tech
+
+ Do not edit the class manually.
+ """
+
+ def __init__(self, api_client=None):
+ if api_client is None:
+ api_client = ApiClient()
+ self.api_client = api_client
+
+ def create_serve(self, **kwargs): # noqa: E501
+ """Create a serve instance # noqa: E501
+
+ This method makes a synchronous HTTP request by default. To make an
+ asynchronous HTTP request, please pass async_req=True
+ >>> thread = api.create_serve(async_req=True)
+ >>> result = thread.get()
+
+ :param async_req bool: execute request asynchronously
+ :param ServeSpec serve_spec:
+ :param _preload_content: if False, the urllib3.HTTPResponse object will
+ be returned without reading/decoding response
+ data. Default is True.
+ :param _request_timeout: timeout setting for this request. If one
+ number provided, it will be total request
+ timeout. It can also be a pair (tuple) of
+ (connection, read) timeouts.
+ :return: JsonResponse
+ If the method is called asynchronously,
+ returns the request thread.
+ """
+ kwargs["_return_http_data_only"] = True
+ return self.create_serve_with_http_info(**kwargs) # noqa: E501
+
+ def create_serve_with_http_info(self, **kwargs): # noqa: E501
+ """Create a serve instance # noqa: E501
+
+ This method makes a synchronous HTTP request by default. To make an
+ asynchronous HTTP request, please pass async_req=True
+ >>> thread = api.create_serve_with_http_info(async_req=True)
+ >>> result = thread.get()
+
+ :param async_req bool: execute request asynchronously
+ :param ServeSpec serve_spec:
+ :param _return_http_data_only: response data without head status code
+ and headers
+ :param _preload_content: if False, the urllib3.HTTPResponse object will
+ be returned without reading/decoding response
+ data. Default is True.
+ :param _request_timeout: timeout setting for this request. If one
+ number provided, it will be total request
+ timeout. It can also be a pair (tuple) of
+ (connection, read) timeouts.
+ :return: tuple(JsonResponse, status_code(int), headers(HTTPHeaderDict))
+ If the method is called asynchronously,
+ returns the request thread.
+ """
+
+ local_var_params = locals()
+
+ all_params = ["serve_spec"]
+ all_params.extend(
+ ["async_req", "_return_http_data_only", "_preload_content", "_request_timeout"]
+ )
+
+ for key, val in six.iteritems(local_var_params["kwargs"]):
+ if key not in all_params:
+ raise ApiTypeError(
+ "Got an unexpected keyword argument '%s' to method create_serve" % key
+ )
+ local_var_params[key] = val
+ del local_var_params["kwargs"]
+
+ collection_formats = {}
+
+ path_params = {}
+
+ query_params = []
+
+ header_params = {}
+
+ form_params = []
+ local_var_files = {}
+
+ body_params = None
+ if "serve_spec" in local_var_params:
+ body_params = local_var_params["serve_spec"]
+ # HTTP header `Accept`
+ header_params["Accept"] = self.api_client.select_header_accept(
+ ["application/json; charset=utf-8"]
+ ) # noqa: E501
+
+ # HTTP header `Content-Type`
+ header_params["Content-Type"] = self.api_client.select_header_content_type( # noqa: E501
+ ["application/yaml", "application/json"]
+ ) # noqa: E501
+
+ # Authentication setting
+ auth_settings = [] # noqa: E501
+
+ return self.api_client.call_api(
+ "/v1/serve",
+ "POST",
+ path_params,
+ query_params,
+ header_params,
+ body=body_params,
+ post_params=form_params,
+ files=local_var_files,
+ response_type="JsonResponse", # noqa: E501
+ auth_settings=auth_settings,
+ async_req=local_var_params.get("async_req"),
+ _return_http_data_only=local_var_params.get("_return_http_data_only"), # noqa: E501
+ _preload_content=local_var_params.get("_preload_content", True),
+ _request_timeout=local_var_params.get("_request_timeout"),
+ collection_formats=collection_formats,
+ )
+
+ def delete_serve(self, **kwargs): # noqa: E501
+ """Delete the serve instance. # noqa: E501
+
+ This method makes a synchronous HTTP request by default. To make an
+ asynchronous HTTP request, please pass async_req=True
+ >>> thread = api.delete_serve(async_req=True)
+ >>> result = thread.get()
+
+ :param async_req bool: execute request asynchronously
+ :param ServeSpec serve_spec:
+ :param _preload_content: if False, the urllib3.HTTPResponse object will
+ be returned without reading/decoding response
+ data. Default is True.
+ :param _request_timeout: timeout setting for this request. If one
+ number provided, it will be total request
+ timeout. It can also be a pair (tuple) of
+ (connection, read) timeouts.
+ :return: JsonResponse
+ If the method is called asynchronously,
+ returns the request thread.
+ """
+ kwargs["_return_http_data_only"] = True
+ return self.delete_serve_with_http_info(**kwargs) # noqa: E501
+
+ def delete_serve_with_http_info(self, **kwargs): # noqa: E501
+ """Delete the serve instance. # noqa: E501
+
+ This method makes a synchronous HTTP request by default. To make an
+ asynchronous HTTP request, please pass async_req=True
+ >>> thread = api.delete_serve_with_http_info(async_req=True)
+ >>> result = thread.get()
+
+ :param async_req bool: execute request asynchronously
+ :param ServeSpec serve_spec:
+ :param _return_http_data_only: response data without head status code
+ and headers
+ :param _preload_content: if False, the urllib3.HTTPResponse object will
+ be returned without reading/decoding response
+ data. Default is True.
+ :param _request_timeout: timeout setting for this request. If one
+ number provided, it will be total request
+ timeout. It can also be a pair (tuple) of
+ (connection, read) timeouts.
+ :return: tuple(JsonResponse, status_code(int), headers(HTTPHeaderDict))
+ If the method is called asynchronously,
+ returns the request thread.
+ """
+
+ local_var_params = locals()
+
+ all_params = ["serve_spec"]
+ all_params.extend(
+ ["async_req", "_return_http_data_only", "_preload_content", "_request_timeout"]
+ )
+
+ for key, val in six.iteritems(local_var_params["kwargs"]):
+ if key not in all_params:
+ raise ApiTypeError(
+ "Got an unexpected keyword argument '%s' to method delete_serve" % key
+ )
+ local_var_params[key] = val
+ del local_var_params["kwargs"]
+
+ collection_formats = {}
+
+ path_params = {}
+
+ query_params = []
+
+ header_params = {}
+
+ form_params = []
+ local_var_files = {}
+
+ body_params = None
+ if "serve_spec" in local_var_params:
+ body_params = local_var_params["serve_spec"]
+ # HTTP header `Accept`
+ header_params["Accept"] = self.api_client.select_header_accept(
+ ["application/json; charset=utf-8"]
+ ) # noqa: E501
+
+ # Authentication setting
+ auth_settings = [] # noqa: E501
+
+ return self.api_client.call_api(
+ "/v1/serve",
+ "DELETE",
+ path_params,
+ query_params,
+ header_params,
+ body=body_params,
+ post_params=form_params,
+ files=local_var_files,
+ response_type="JsonResponse", # noqa: E501
+ auth_settings=auth_settings,
+ async_req=local_var_params.get("async_req"),
+ _return_http_data_only=local_var_params.get("_return_http_data_only"), # noqa: E501
+ _preload_content=local_var_params.get("_preload_content", True),
+ _request_timeout=local_var_params.get("_request_timeout"),
+ collection_formats=collection_formats,
+ )
+
+ def ping2(self, **kwargs): # noqa: E501
+ """Ping submarine server # noqa: E501
+
+ Return the Pong message for test the connectivity # noqa: E501
+ This method makes a synchronous HTTP request by default. To make an
+ asynchronous HTTP request, please pass async_req=True
+ >>> thread = api.ping2(async_req=True)
+ >>> result = thread.get()
+
+ :param async_req bool: execute request asynchronously
+ :param _preload_content: if False, the urllib3.HTTPResponse object will
+ be returned without reading/decoding response
+ data. Default is True.
+ :param _request_timeout: timeout setting for this request. If one
+ number provided, it will be total request
+ timeout. It can also be a pair (tuple) of
+ (connection, read) timeouts.
+ :return: str
+ If the method is called asynchronously,
+ returns the request thread.
+ """
+ kwargs["_return_http_data_only"] = True
+ return self.ping2_with_http_info(**kwargs) # noqa: E501
+
+ def ping2_with_http_info(self, **kwargs): # noqa: E501
+ """Ping submarine server # noqa: E501
+
+ Return the Pong message for test the connectivity # noqa: E501
+ This method makes a synchronous HTTP request by default. To make an
+ asynchronous HTTP request, please pass async_req=True
+ >>> thread = api.ping2_with_http_info(async_req=True)
+ >>> result = thread.get()
+
+ :param async_req bool: execute request asynchronously
+ :param _return_http_data_only: response data without head status code
+ and headers
+ :param _preload_content: if False, the urllib3.HTTPResponse object will
+ be returned without reading/decoding response
+ data. Default is True.
+ :param _request_timeout: timeout setting for this request. If one
+ number provided, it will be total request
+ timeout. It can also be a pair (tuple) of
+ (connection, read) timeouts.
+ :return: tuple(str, status_code(int), headers(HTTPHeaderDict))
+ If the method is called asynchronously,
+ returns the request thread.
+ """
+
+ local_var_params = locals()
+
+ all_params = []
+ all_params.extend(
+ ["async_req", "_return_http_data_only", "_preload_content", "_request_timeout"]
+ )
+
+ for key, val in six.iteritems(local_var_params["kwargs"]):
+ if key not in all_params:
+ raise ApiTypeError("Got an unexpected keyword argument '%s' to method ping2" % key)
+ local_var_params[key] = val
+ del local_var_params["kwargs"]
+
+ collection_formats = {}
+
+ path_params = {}
+
+ query_params = []
+
+ header_params = {}
+
+ form_params = []
+ local_var_files = {}
+
+ body_params = None
+ # HTTP header `Accept`
+ header_params["Accept"] = self.api_client.select_header_accept(
+ ["application/json; charset=utf-8"]
+ ) # noqa: E501
+
+ # Authentication setting
+ auth_settings = [] # noqa: E501
+
+ return self.api_client.call_api(
+ "/v1/serve/ping",
+ "GET",
+ path_params,
+ query_params,
+ header_params,
+ body=body_params,
+ post_params=form_params,
+ files=local_var_files,
+ response_type="str", # noqa: E501
+ auth_settings=auth_settings,
+ async_req=local_var_params.get("async_req"),
+ _return_http_data_only=local_var_params.get("_return_http_data_only"), # noqa: E501
+ _preload_content=local_var_params.get("_preload_content", True),
+ _request_timeout=local_var_params.get("_request_timeout"),
+ collection_formats=collection_formats,
+ )
diff --git a/submarine-sdk/pysubmarine/submarine/client/api/serve_client.py b/submarine-sdk/pysubmarine/submarine/client/api/serve_client.py
new file mode 100644
index 0000000..545ea1f
--- /dev/null
+++ b/submarine-sdk/pysubmarine/submarine/client/api/serve_client.py
@@ -0,0 +1,53 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import logging
+
+from submarine.client.api.serve_api import ServeApi
+from submarine.client.utils.api_utils import generate_host, get_api_client
+
+logger = logging.getLogger(__name__)
+logging.basicConfig(format="%(message)s")
+logging.getLogger().setLevel(logging.INFO)
+
+
+class ServeClient:
+ def __init__(self, host: str = generate_host()) -> None:
+ """
+ Submarine serve client constructor
+ :param host: An HTTP URI like http://submarine-server:8080.
+ """
+ api_client = get_api_client(host)
+ self.serve_api = ServeApi(api_client=api_client)
+
+ def create_serve(self, model_name: str, model_version: int, async_req: bool):
+ """
+ Create a model serve
+ :param model_name: Name of a registered model
+ :param model_version: Version of a registered model
+ """
+ serve_spec = {"modelName": model_name, "modelVersion": model_version}
+ response = self.serve_api.create_serve(serve_spec=serve_spec, async_req=async_req)
+ return response
+
+ def delete_serve(self, model_name: str, model_version: int, async_req: bool):
+ """
+ Delete a serving model
+ :param model_name: Name of a registered model
+ :param model_version: Version of a registered model
+ """
+ serve_spec = {"modelName": model_name, "modelVersion": model_version}
+ response = self.serve_api.delete_serve(serve_spec=serve_spec, async_req=async_req)
+ return response
diff --git a/submarine-sdk/pysubmarine/submarine/client/models/__init__.py b/submarine-sdk/pysubmarine/submarine/client/models/__init__.py
index 005b99b..0365a1d 100644
--- a/submarine-sdk/pysubmarine/submarine/client/models/__init__.py
+++ b/submarine-sdk/pysubmarine/submarine/client/models/__init__.py
@@ -41,3 +41,4 @@ from submarine.client.models.kernel_spec import KernelSpec
from submarine.client.models.notebook_meta import NotebookMeta
from submarine.client.models.notebook_pod_spec import NotebookPodSpec
from submarine.client.models.notebook_spec import NotebookSpec
+from submarine.client.models.serve_spec import ServeSpec
diff --git a/submarine-sdk/pysubmarine/submarine/client/models/serve_spec.py b/submarine-sdk/pysubmarine/submarine/client/models/serve_spec.py
new file mode 100644
index 0000000..c91e4b6
--- /dev/null
+++ b/submarine-sdk/pysubmarine/submarine/client/models/serve_spec.py
@@ -0,0 +1,223 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+# coding: utf-8
+
+"""
+ Submarine API
+
+ The Submarine REST API allows you to access Submarine resources such as, experiments, environments and notebooks. The API is hosted under the /v1 path on the Submarine server. For example, to list experiments on a server hosted at http://localhost:8080, access http://localhost:8080/api/v1/experiment/ # noqa: E501
+
+ The version of the OpenAPI document: 0.7.0-SNAPSHOT
+ Contact: dev@submarine.apache.org
+ Generated by: https://openapi-generator.tech
+"""
+
+
+import pprint
+import re # noqa: F401
+
+import six
+
+from submarine.client.configuration import Configuration
+
+
+class ServeSpec(object):
+ """NOTE: This class is auto generated by OpenAPI Generator.
+ Ref: https://openapi-generator.tech
+
+ Do not edit the class manually.
+ """
+
+ """
+ Attributes:
+ openapi_types (dict): The key is attribute name
+ and the value is attribute type.
+ attribute_map (dict): The key is attribute name
+ and the value is json key in definition.
+ """
+ openapi_types = {
+ "model_name": "str",
+ "model_version": "int",
+ "model_type": "str",
+ "model_uri": "str",
+ }
+
+ attribute_map = {
+ "model_name": "modelName",
+ "model_version": "modelVersion",
+ "model_type": "modelType",
+ "model_uri": "modelURI",
+ }
+
+ def __init__(
+ self,
+ model_name=None,
+ model_version=None,
+ model_type=None,
+ model_uri=None,
+ local_vars_configuration=None,
+ ): # noqa: E501
+ """ServeSpec - a model defined in OpenAPI""" # noqa: E501
+ if local_vars_configuration is None:
+ local_vars_configuration = Configuration()
+ self.local_vars_configuration = local_vars_configuration
+
+ self._model_name = None
+ self._model_version = None
+ self._model_type = None
+ self._model_uri = None
+ self.discriminator = None
+
+ if model_name is not None:
+ self.model_name = model_name
+ if model_version is not None:
+ self.model_version = model_version
+ if model_type is not None:
+ self.model_type = model_type
+ if model_uri is not None:
+ self.model_uri = model_uri
+
+ @property
+ def model_name(self):
+ """Gets the model_name of this ServeSpec. # noqa: E501
+
+
+ :return: The model_name of this ServeSpec. # noqa: E501
+ :rtype: str
+ """
+ return self._model_name
+
+ @model_name.setter
+ def model_name(self, model_name):
+ """Sets the model_name of this ServeSpec.
+
+
+ :param model_name: The model_name of this ServeSpec. # noqa: E501
+ :type: str
+ """
+
+ self._model_name = model_name
+
+ @property
+ def model_version(self):
+ """Gets the model_version of this ServeSpec. # noqa: E501
+
+
+ :return: The model_version of this ServeSpec. # noqa: E501
+ :rtype: int
+ """
+ return self._model_version
+
+ @model_version.setter
+ def model_version(self, model_version):
+ """Sets the model_version of this ServeSpec.
+
+
+ :param model_version: The model_version of this ServeSpec. # noqa: E501
+ :type: int
+ """
+
+ self._model_version = model_version
+
+ @property
+ def model_type(self):
+ """Gets the model_type of this ServeSpec. # noqa: E501
+
+
+ :return: The model_type of this ServeSpec. # noqa: E501
+ :rtype: str
+ """
+ return self._model_type
+
+ @model_type.setter
+ def model_type(self, model_type):
+ """Sets the model_type of this ServeSpec.
+
+
+ :param model_type: The model_type of this ServeSpec. # noqa: E501
+ :type: str
+ """
+
+ self._model_type = model_type
+
+ @property
+ def model_uri(self):
+ """Gets the model_uri of this ServeSpec. # noqa: E501
+
+
+ :return: The model_uri of this ServeSpec. # noqa: E501
+ :rtype: str
+ """
+ return self._model_uri
+
+ @model_uri.setter
+ def model_uri(self, model_uri):
+ """Sets the model_uri of this ServeSpec.
+
+
+ :param model_uri: The model_uri of this ServeSpec. # noqa: E501
+ :type: str
+ """
+
+ self._model_uri = model_uri
+
+ def to_dict(self):
+ """Returns the model properties as a dict"""
+ result = {}
+
+ for attr, _ in six.iteritems(self.openapi_types):
+ value = getattr(self, attr)
+ if isinstance(value, list):
+ result[attr] = list(
+ map(lambda x: x.to_dict() if hasattr(x, "to_dict") else x, value)
+ )
+ elif hasattr(value, "to_dict"):
+ result[attr] = value.to_dict()
+ elif isinstance(value, dict):
+ result[attr] = dict(
+ map(
+ lambda item: (item[0], item[1].to_dict())
+ if hasattr(item[1], "to_dict")
+ else item,
+ value.items(),
+ )
+ )
+ else:
+ result[attr] = value
+
+ return result
+
+ def to_str(self):
+ """Returns the string representation of the model"""
+ return pprint.pformat(self.to_dict())
+
+ def __repr__(self):
+ """For `print` and `pprint`"""
+ return self.to_str()
+
+ def __eq__(self, other):
+ """Returns true if both objects are equal"""
+ if not isinstance(other, ServeSpec):
+ return False
+
+ return self.to_dict() == other.to_dict()
+
+ def __ne__(self, other):
+ """Returns true if both objects are not equal"""
+ if not isinstance(other, ServeSpec):
+ return True
+
+ return self.to_dict() != other.to_dict()
diff --git a/submarine-sdk/pysubmarine/submarine/client/api/__init__.py b/submarine-sdk/pysubmarine/submarine/client/utils/api_utils.py
similarity index 54%
copy from submarine-sdk/pysubmarine/submarine/client/api/__init__.py
copy to submarine-sdk/pysubmarine/submarine/client/utils/api_utils.py
index 19defd6..9a7f6ae 100644
--- a/submarine-sdk/pysubmarine/submarine/client/api/__init__.py
+++ b/submarine-sdk/pysubmarine/submarine/client/utils/api_utils.py
@@ -13,11 +13,26 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-from __future__ import absolute_import
+import os
-# import apis into api package
-from submarine.client.api.environment_api import EnvironmentApi
-from submarine.client.api.experiment_api import ExperimentApi
-from submarine.client.api.notebook_api import NotebookApi
+from submarine.client.api_client import ApiClient
+from submarine.client.configuration import Configuration
-# flake8: noqa
+
+def generate_host() -> str:
+ """
+ Generate submarine host
+ :return: submarine host
+ """
+ submarine_server_dns_name = str(os.environ.get("SUBMARINE_SERVER_DNS_NAME"))
+ submarine_server_port = str(os.environ.get("SUBMARINE_SERVER_PORT"))
+ host = "http://" + submarine_server_dns_name + ":" + submarine_server_port
+ return host
+
+
+def get_api_client(host: str) -> ApiClient:
+ configuration = Configuration()
+ configuration.host = host + "/api"
+ api_client = ApiClient(configuration=configuration)
+
+ return api_client
diff --git a/submarine-sdk/pysubmarine/submarine/models/pytorch.py b/submarine-sdk/pysubmarine/submarine/models/pytorch.py
index 38cdd57..1b45d30 100644
--- a/submarine-sdk/pysubmarine/submarine/models/pytorch.py
+++ b/submarine-sdk/pysubmarine/submarine/models/pytorch.py
@@ -18,5 +18,7 @@ import os
import torch
-def save_model(model, artifact_path: str):
- torch.save(model, os.path.join(artifact_path, "model.pth"))
+def save_model(model, artifact_path: str, input_dim: list) -> None:
+ example_forward_example = torch.rand(input_dim)
+ scripted_model = torch.jit.trace(model, example_forward_example)
+ scripted_model.save(model, os.path.join(artifact_path, "model.pth"))
diff --git a/submarine-sdk/pysubmarine/submarine/tracking/client.py b/submarine-sdk/pysubmarine/submarine/tracking/client.py
index f563254..76f350a 100644
--- a/submarine-sdk/pysubmarine/submarine/tracking/client.py
+++ b/submarine-sdk/pysubmarine/submarine/tracking/client.py
@@ -21,6 +21,8 @@ from typing import Any, Dict
import submarine
from submarine.artifacts.repository import Repository
+from submarine.client.api.serve_client import ServeClient
+from submarine.client.utils.api_utils import generate_host
from submarine.entities import Metric, Param
from submarine.exceptions import SubmarineException
from submarine.tracking import utils
@@ -40,6 +42,7 @@ class SubmarineClient(object):
s3_registry_uri: str = None,
aws_access_key_id: str = None,
aws_secret_access_key: str = None,
+ host: str = generate_host(),
) -> None:
"""
:param db_uri: Address of local or remote tracking server. If not provided, defaults
@@ -54,6 +57,7 @@ class SubmarineClient(object):
self.db_uri = db_uri or submarine.get_db_uri()
self.store = utils.get_tracking_sqlalchemy_store(self.db_uri)
self.model_registry = utils.get_model_registry_sqlalchemy_store(self.db_uri)
+ self.serve_client = ServeClient(host)
def log_metric(
self,
@@ -130,7 +134,7 @@ class SubmarineClient(object):
"Saving pytorch model needs to provide input and output dimension for"
" serving."
)
- submarine.models.pytorch.save_model(model, model_save_dir)
+ submarine.models.pytorch.save_model(model, model_save_dir, input_dim)
elif model_type == "tensorflow":
import submarine.models.tensorflow
@@ -171,3 +175,19 @@ class SubmarineClient(object):
experiment_id=utils.get_job_id(),
model_type=model_type,
)
+
+ def create_serve(self, model_name: str, model_version: int, async_req: bool = True):
+ """
+ Create serve of a model through Seldon Core
+ :param model_name: Name of a registered model
+ :param model_version: Version of a registered model
+ """
+ return self.serve_client.create_serve(model_name, model_version, async_req)
+
+ def delete_serve(self, model_name: str, model_version: int, async_req: bool = True):
+ """
+ Delete a serving model
+ :param model_name: Name of a registered model
+ :param model_version: Version of a registered model
+ """
+ return self.serve_client.delete_serve(model_name, model_version, async_req=async_req)
diff --git a/submarine-sdk/pysubmarine/submarine/tracking/fluent.py b/submarine-sdk/pysubmarine/submarine/tracking/fluent.py
index dce8cfe..ec9ed9e 100644
--- a/submarine-sdk/pysubmarine/submarine/tracking/fluent.py
+++ b/submarine-sdk/pysubmarine/submarine/tracking/fluent.py
@@ -73,3 +73,22 @@ def save_model(
SubmarineClient().save_model(
model_type, model, artifact_path, registered_model_name, input_dim, output_dim
)
+ SubmarineClient().save_model(model_type, model, artifact_path, registered_model_name)
+
+
+def create_serve(model_name: str, model_version: int):
+ """
+ Create serve of a model through Seldon Core
+ :param model_name: Name of a registered model
+ :param model_version: Version of a registered model
+ """
+ SubmarineClient().create_serve(model_name, model_version)
+
+
+def delete_serve(model_name: str, model_version: int):
+ """
+ Delete a serving model
+ :param model_name: Name of a registered model
+ :param model_version: Version of a registered model
+ """
+ SubmarineClient().delete_serve(model_name, model_version)
diff --git a/submarine-server/server-core/src/main/java/org/apache/submarine/server/Bootstrap.java b/submarine-server/server-core/src/main/java/org/apache/submarine/server/Bootstrap.java
index 76e3ab4..a373558 100644
--- a/submarine-server/server-core/src/main/java/org/apache/submarine/server/Bootstrap.java
+++ b/submarine-server/server-core/src/main/java/org/apache/submarine/server/Bootstrap.java
@@ -66,7 +66,8 @@ public class Bootstrap extends HttpServlet {
.collect(Collectors.toSet()))
.resourceClasses(Stream.of("org.apache.submarine.server.rest.NotebookRestApi",
"org.apache.submarine.server.rest.ExperimentRestApi",
- "org.apache.submarine.server.rest.EnvironmentRestApi")
+ "org.apache.submarine.server.rest.EnvironmentRestApi",
+ "org.apache.submarine.server.rest.ServeRestApi")
.collect(Collectors.toSet()));
try {
---------------------------------------------------------------------
To unsubscribe, e-mail: dev-unsubscribe@submarine.apache.org
For additional commands, e-mail: dev-help@submarine.apache.org