diff --git a/README.md b/README.md index 21381ec..5c86caf 100644 --- a/README.md +++ b/README.md @@ -215,7 +215,7 @@ algorithm.init({"data": "https://i.imgur.com/bXdORXl.jpeg"}) Model Manifests are optional files that you can provide to your algorithm to easily define important model files, their locations; and metadata - this file is called `model_manifest.json`. -```python +```json { "required_files" : [ { "name": "squeezenet", @@ -249,7 +249,7 @@ define important model files, their locations; and metadata - this file is calle With the Model Manifest system, you're also able to "freeze" your model_manifest.json, creating a model_manifest.json.freeze. This file encodes the hash of the model file, preventing tampering once frozen - forver locking a version of your algorithm code with your model file. -```python +```json { "required_files":[ { @@ -298,7 +298,7 @@ For this, you'll need to select an MLOps Enabled Environment; and you will need Once setup, you will need to define your `mlops.json` file, including your deployment and model ids. -```python +```json { "model_id": "YOUR_MODEL_ID", "deployment_id": "YOUR_DEPLOYMENT_ID", @@ -345,9 +345,6 @@ algorithm.init(0.25, mlops=True) ``` -report_deployment_stats() - - ## Readme publishing diff --git a/README_template.md b/README_template.md index ded0700..3c47c6a 100644 --- a/README_template.md +++ b/README_template.md @@ -61,12 +61,12 @@ Check out these examples to help you get started: Model Manifests are optional files that you can provide to your algorithm to easily define important model files, their locations; and metadata - this file is called `model_manifest.json`. -```python +```json ``` With the Model Manifest system, you're also able to "freeze" your model_manifest.json, creating a model_manifest.json.freeze. This file encodes the hash of the model file, preventing tampering once frozen - forver locking a version of your algorithm code with your model file. -```python +```json ``` As you can link to both hosted data collections, and AWS/GCP/Azure based block storage media, you're able to link your algorithm code with your model files, wherever they live today. @@ -81,7 +81,7 @@ For this, you'll need to select an MLOps Enabled Environment; and you will need Once setup, you will need to define your `mlops.json` file, including your deployment and model ids. -```python +```json ``` Along with defining your `DATAROBOT_MLOPS_API_TOKEN` as a secret to your Algorithm, you're ready to start sending MLOps data back to DataRobot! @@ -90,9 +90,6 @@ Along with defining your `DATAROBOT_MLOPS_API_TOKEN` as a secret to your Algorit ```python ``` -report_deployment_stats() - - ## Readme publishing diff --git a/adk/io.py b/adk/io.py index be2045c..ccaabfc 100644 --- a/adk/io.py +++ b/adk/io.py @@ -57,7 +57,7 @@ def create_exception(exception, loading_exception=False): response = json.dumps({ "error": { "message": str(exception), - "stacktrace": traceback.format_exc(), + "stacktrace": " ".join(traceback.format_exception(etype=type(exception), value=exception, tb=exception.__traceback__)), "error_type": error_type, } }) diff --git a/adk/mlops.py b/adk/mlops.py index a64efbd..cd335a7 100644 --- a/adk/mlops.py +++ b/adk/mlops.py @@ -8,20 +8,35 @@ class MLOps(object): spool_dir = "/tmp/ta" agent_dir = "/opt/mlops-agent" mlops_dir_name = "datarobot_mlops_package-8.1.2" + total_dir_path = agent_dir + "/" + mlops_dir_name def __init__(self, api_token, path): self.token = api_token if os.path.exists(path): with open(path) as f: mlops_config = json.load(f) - else: - raise Exception("'mlops.json' file does not exist, but mlops was requested.") + self.endpoint = mlops_config['datarobot_mlops_service_url'] + self.model_id = mlops_config['model_id'] + self.deployment_id = mlops_config['deployment_id'] + self.mlops_name = mlops_config.get('mlops_dir_name', 'datarobot_mlops_package-8.1.2') + if "MLOPS_SERVICE_URL" in os.environ: + self.endpoint = os.environ['MLOPS_SERVICE_URL'] + if "MODEL_ID" in os.environ: + self.model_id = os.environ['MODEL_ID'] + if "DEPLOYMENT_ID" in os.environ: + self.deployment_id = os.environ['DEPLOYMENT_ID'] if not os.path.exists(self.agent_dir): raise Exception("environment is not configured for mlops.\nPlease select a valid mlops enabled environment.") - self.endpoint = mlops_config['datarobot_mlops_service_url'] - self.model_id = mlops_config['model_id'] - self.deployment_id = mlops_config['deployment_id'] - self.mlops_name = mlops_config.get('mlops_dir_name', 'datarobot_mlops_package-8.1.2') + + if self.endpoint is None: + raise Exception("'no endpoint found, please add 'MLOPS_SERVICE_URL' environment variable, or create an " + "mlops.json file") + if self.model_id is None: + raise Exception("no model_id found, please add 'MODEL_ID' environment variable, or create an mlops.json " + "file") + if self.deployment_id is None: + raise Exception("no deployment_id found, please add 'DEPLOYMENT_ID' environment variable, or create an " + "mlops.json file") def init(self): os.environ['MLOPS_DEPLOYMENT_ID'] = self.deployment_id @@ -29,18 +44,18 @@ def init(self): os.environ['MLOPS_SPOOLER_TYPE'] = "FILESYSTEM" os.environ['MLOPS_FILESYSTEM_DIRECTORY'] = self.spool_dir - with open(f'{self.agent_dir}/{self.mlops_dir_name}/conf/mlops.agent.conf.yaml') as f: + with open(self.total_dir_path + '/conf/mlops.agent.conf.yaml') as f: documents = yaml.load(f, Loader=yaml.FullLoader) documents['mlopsUrl'] = self.endpoint documents['apiToken'] = self.token - with open(f'{self.agent_dir}/{self.mlops_dir_name}/conf/mlops.agent.conf.yaml', 'w') as f: + with open(self.total_dir_path + '/conf/mlops.agent.conf.yaml', 'w') as f: yaml.dump(documents, f) - subprocess.call(f'{self.agent_dir}/{self.mlops_dir_name}/bin/start-agent.sh') - check = subprocess.Popen([f'{self.agent_dir}/{self.mlops_dir_name}/bin/status-agent.sh'], stdout=subprocess.PIPE) + subprocess.call(self.total_dir_path + '/bin/start-agent.sh') + check = subprocess.Popen([self.total_dir_path + '/bin/status-agent.sh'], stdout=subprocess.PIPE) output = check.stdout.readlines()[0] check.terminate() if b"DataRobot MLOps-Agent is running as a service." in output: return True else: - raise Exception(output) \ No newline at end of file + raise Exception(output) diff --git a/adk/modeldata.py b/adk/modeldata.py index e2b49cd..893d28b 100644 --- a/adk/modeldata.py +++ b/adk/modeldata.py @@ -56,37 +56,42 @@ def initialize(self): self.models[name] = FileData(real_hash, local_data_path) def get_model(self, model_name): - if model_name in self.models: - return self.models[model_name].file_path - elif len([optional for optional in self.manifest_data['optional_files'] if - optional['name'] == model_name]) > 0: - self.find_optional_model(model_name) - return self.models[model_name].file_path + if self.available(): + if model_name in self.models: + return self.models[model_name].file_path + elif len([optional for optional in self.manifest_data['optional_files'] if + optional['name'] == model_name]) > 0: + self.find_optional_model(model_name) + return self.models[model_name].file_path + else: + raise Exception("model name " + model_name + " not found in manifest") else: - raise Exception("model name " + model_name + " not found in manifest") + raise Exception("unable to get model {}, model_manifest.json not found.".format(model_name)) def find_optional_model(self, file_name): - - found_models = [optional for optional in self.manifest_data['optional_files'] if - optional['name'] == file_name] - if len(found_models) == 0: - raise Exception("file with name '" + file_name + "' not found in model manifest.") - model_info = found_models[0] - self.models[file_name] = {} - source_uri = model_info['source_uri'] - fail_on_tamper = model_info.get("fail_on_tamper", False) - expected_hash = model_info.get('md5_checksum', None) - with self.client.file(source_uri).getFile() as f: - local_data_path = f.name - real_hash = md5_for_file(local_data_path) - if self.using_frozen: - if real_hash != expected_hash and fail_on_tamper: - raise Exception("Model File Mismatch for " + file_name + - "\nexpected hash: " + expected_hash + "\nreal hash: " + real_hash) + if self.available(): + found_models = [optional for optional in self.manifest_data['optional_files'] if + optional['name'] == file_name] + if len(found_models) == 0: + raise Exception("file with name '" + file_name + "' not found in model manifest.") + model_info = found_models[0] + self.models[file_name] = {} + source_uri = model_info['source_uri'] + fail_on_tamper = model_info.get("fail_on_tamper", False) + expected_hash = model_info.get('md5_checksum', None) + with self.client.file(source_uri).getFile() as f: + local_data_path = f.name + real_hash = md5_for_file(local_data_path) + if self.using_frozen: + if real_hash != expected_hash and fail_on_tamper: + raise Exception("Model File Mismatch for " + file_name + + "\nexpected hash: " + expected_hash + "\nreal hash: " + real_hash) + else: + self.models[file_name] = FileData(real_hash, local_data_path) else: self.models[file_name] = FileData(real_hash, local_data_path) else: - self.models[file_name] = FileData(real_hash, local_data_path) + raise Exception("unable to get model {}, model_manifest.json not found.".format(model_name)) def get_manifest(self): if os.path.exists(self.manifest_frozen_path): diff --git a/tests/test_adk_local.py b/tests/test_adk_local.py index 4b5585c..d4ed78a 100644 --- a/tests/test_adk_local.py +++ b/tests/test_adk_local.py @@ -135,17 +135,18 @@ def test_manifest_file_success(self): self.assertEqual(expected_output, actual_output) def test_manifest_file_tampered(self): - input = "Algorithmia" + input = 'Algorithmia' expected_output = {"error": {"error_type": "LoadingError", "message": "Model File Mismatch for squeezenet\n" "expected hash: f20b50b44fdef367a225d41f747a0963\n" "real hash: 46a44d32d2c5c07f7f66324bef4c7266", - "stacktrace": "NoneType: None\n"}} + "stacktrace": ''}} actual_output = json.loads(self.execute_manifest_example(input, apply_successful_manifest_parsing, loading_with_manifest, manifest_path="tests/manifests/bad_model_manifest" ".json")) + actual_output['error']['stacktrace'] = '' self.assertEqual(expected_output, actual_output)