11# -*- coding: utf-8 -*-
22"""Classes for interacting with models."""
33
4+ import re
5+ import json
46import logging
57from datetime import datetime
68from ._api_object import ApiObject
79from urllib .parse import urlencode
8- from .error import NotFoundError , ResponseError
9- from typing import Union
10+ from .error import NotFoundError , ResponseError , BadRequestError
1011from time import time as t
1112from time import sleep
13+ from ._util import load_model , upload_input_example , run_model , deploy_model
1214
1315class Models :
1416 """The `Models` object.
@@ -30,6 +32,24 @@ def __init__(self, api_client):
3032 """
3133 self ._api_client = api_client
3234 self .logger = logging .getLogger (__name__ )
35+ # model deployment specific instance variables
36+ self .container_registry_regex = "^((?:[A-Za-z0-9-_]+)(?:\.[A-Za-z0-9-_]+)+\/)?([^:]+)(?::(.+))?$"
37+ self .default_inputs = [
38+ {
39+ "name" : "input" ,
40+ "acceptedMediaTypes" : "application/json" ,
41+ "maximumSize" : 1000000 ,
42+ "description" : "Default input data"
43+ }
44+ ]
45+ self .default_outputs = [
46+ {
47+ "name" : "results.json" ,
48+ "mediaType" : "application/json" ,
49+ "maximumSize" : 1000000 ,
50+ "description" : "Default output data"
51+ }
52+ ]
3353
3454 def get_model_processing_details (self , model , version ):
3555 """
@@ -47,7 +67,6 @@ def get_model_processing_details(self, model, version):
4767 """
4868 model_id = Model ._coerce_identifier (model )
4969
50- # TODO: this was moved from the models api to the resources api, perhaps it should go in a different module?
5170 endpoint = "/resources/processing/models"
5271
5372 result = self ._api_client .http .get (endpoint )
@@ -363,6 +382,127 @@ def get_models(self, model_id=None, author=None, created_by_email=None, name=Non
363382 json_list = self ._api_client .http .get ('{}?{}' .format (self ._base_route , urlencode (body )))
364383 return list (Model (json_obj , self ._api_client ) for json_obj in json_list )
365384
385+ def deploy (
386+ self , container_image , model_name , model_version , sample_input_file , credentials = None ,
387+ model_id = None , run_timeout = None , status_timeout = None , short_description = None , tags = [],
388+ gpu = False , long_description = None , technical_details = None , performance_summary = None ,
389+ performance_metrics = None , input_details = None , output_details = None
390+ ):
391+ """Deploys a new `Model` instance.
392+
393+ Args:
394+ container_image (str): Docker container image to be deployed. This string should represent what follows a `docker pull` command
395+ model_name (str): Name of model to be deployed
396+ model_version (str): Version of model to be deployed
397+ sample_input_file (str): Path to local file to be used for sample inference
398+ credentials (dict): Dictionary containing credentials if the container image is private. The keys in this dictionary must be `["user", "pass"]`
399+ model_id (str): Model identifier if deploying a new version to a model that already exists
400+ run_timeout (str): Timeout threshold for container `run` route
401+ status_timeout (str): Timeout threshold for container `status` route
402+ short_description (str): Short description to appear on model biography page
403+ tags (list): List of tags to make model more discoverable in model library
404+ gpu (bool): Flag for whether or not model requires GPU to run
405+ long_description (str): Description to appear on model biography page
406+ technical_details (str): Technical details to appear on model biography page. Markdown is accepted
407+ performance_summary (str): Description providing model performance to appear on model biography page
408+ performance_metrics (List): List of arrays describing model performance statistics
409+ input_details (List): List of dictionaries describing details of model inputs
410+ output_details (List): List of dictionaries describing details of model outputs
411+
412+ Returns:
413+ dict: Newly deployed model information including formatted URL to newly deployed model page.
414+ Raises:
415+ ApiError: A subclass of ApiError will be raised if the API returns an error status,
416+ or the client is unable to connect.
417+ """
418+ # generate model identifier and version to create new model
419+ if model_id :
420+ identifier , version = model_id , model_version
421+ # create new version of existing model
422+ data = {"version" : version }
423+ try :
424+ response = self ._api_client .http .post (f"{ self ._base_route } /{ identifier } /versions" , data )
425+ except BadRequestError as e :
426+ raise e
427+ else :
428+ # create new model object
429+ data = {'name' : model_name , 'version' : model_version }
430+ response = self ._api_client .http .post (self ._base_route , data )
431+ identifier , version = response .get ('identifier' ), model_version
432+
433+ self .logger .info (f"Created Model Version: { identifier } , { version } " )
434+
435+ # add tags and description
436+ tags_and_description = {
437+ 'description' : short_description or ''
438+ }
439+ if len (tags ) > 0 :
440+ tags_and_description ['tags' ] = tags
441+ response = self ._api_client .http .patch (f"{ self ._base_route } /{ identifier } " , tags_and_description )
442+
443+ # upload container image
444+ m = re .search (self .container_registry_regex , container_image )
445+ domain = m .group (1 ) or "registry.hub.docker.com/"
446+ repository = m .group (2 )
447+ tag = m .group (3 ) or "latest"
448+ image_url = "https://{}v2/{}/manifests/{}" .format (domain , repository , tag )
449+ registry = {'registry' : {'url' : image_url , 'username' : credentials ['user' ], 'password' : credentials ['pass' ]}} if credentials else {'registry' : {'url' : image_url }}
450+ response = self ._api_client .http .post (f"{ self ._base_route } /{ identifier } /versions/{ version } /container-image" , registry )
451+ self .logger .info ("Uploaded Container Image" )
452+
453+ # add model metadata
454+ run_timeout_body = int (run_timeout )* 1000 if run_timeout else 60000
455+ status_timeout_body = int (status_timeout )* 1000 if status_timeout else 60000
456+
457+ model_metadata = {
458+ "requirement" : {"requirementId" : - 6 if gpu else 1 },
459+ "timeout" : {
460+ "run" : run_timeout_body ,
461+ "status" : status_timeout_body
462+ },
463+ "inputs" : input_details or self .default_inputs ,
464+ "outputs" : output_details or self .default_outputs ,
465+ "statistics" : performance_metrics or [],
466+ "processing" : {
467+ "minimumParallelCapacity" : 0 ,
468+ "maximumParallelCapacity" : 1
469+ },
470+ "longDescription" : long_description or "" ,
471+ "technicalDetails" : technical_details or "" ,
472+ "performanceSummary" : performance_summary or ""
473+ }
474+ model_data = self ._api_client .http .patch (f"{ self ._base_route } /{ identifier } /versions/{ version } " , model_metadata )
475+ self .logger .info (f"Model Data: { json .dumps (model_data )} " )
476+
477+ # load model container
478+ try :
479+ load_model (self ._api_client , self .logger , identifier , version )
480+ except Exception as e :
481+ raise ValueError ("Loading model container failed. Make sure you passed through a valid Docker registry container image. \n \n See full error below:\n {}" .format (e ))
482+ # upload sample data for inference test
483+ try :
484+ upload_input_example (self ._api_client , self .logger , identifier , version , model_data , sample_input_file )
485+ except Exception as e :
486+ raise ValueError ("Uploading sample input failed. \n \n See full error below:\n {}" .format (e ))
487+ # run sample inference
488+ try :
489+ run_model (self ._api_client , self .logger , identifier , version )
490+ except Exception as e :
491+ raise ValueError ("Inference test failed. Make sure the provided input sample is valid and your model can process it for inference. \n \n See full error below:\n {}" .format (e ))
492+ # deploy model pending all tests have passed
493+ try :
494+ deploy_model (self ._api_client , self .logger , identifier , version )
495+ except Exception as e :
496+ raise ValueError ("Deployment failed. Check to make sure all of your parameters and assets are valid and try again. \n \n See full error below:\n {}" .format (e ))
497+
498+ # get new model URL and return model data
499+ base_url = self ._api_client .base_url .split ("api" )[0 ][:- 1 ]
500+ container_data = {
501+ 'model_data' : json .dumps (model_data ),
502+ 'container_url' : f"{ base_url } { self ._base_route } /{ identifier } /{ version } "
503+ }
504+ return container_data
505+
366506
367507class Model (ApiObject ):
368508 """A model object.
0 commit comments