diff --git a/.github/workflows/build-test-deploy.yml b/.github/workflows/build-test-deploy.yml index a0072348..87fac0e2 100644 --- a/.github/workflows/build-test-deploy.yml +++ b/.github/workflows/build-test-deploy.yml @@ -22,8 +22,7 @@ jobs: strategy: matrix: python-version: ['3.8', '3.9', '3.10', '3.11'] - os-version: ['ubuntu-20.04', 'windows-latest', 'macos-latest'] -# Pinned Ubuntu version to 20.04 since no Python 3.6 builds available on ubuntu-latest (22.04) as of 2022-12-7. + os-version: ['ubuntu-latest', 'windows-latest', 'macos-latest'] # os-version: [ubuntu-latest, windows-latest, macos-latest] steps: diff --git a/CHANGELOG.md b/CHANGELOG.md index f955f4e9..75252845 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +v1.11.4 (2025-05-02) +-------------------- +**Improvements** +- Improved `upload_local_model` to allow for SAS Model Manager to properly intake local ASTORE models. + +v1.11.3 (2025-04-29) +-------------------- +**Improvements** +- Added `upload_local_model` to `tasks.py`, which can be used to upload local directories to SAS Model Manager without any file generation. + v1.11.2 (2025-04-08) -------------------- **Bugfixes** diff --git a/examples/pzmm_generate_complete_model_card.ipynb b/examples/pzmm_generate_complete_model_card.ipynb index 3a68271b..f49efd56 100644 --- a/examples/pzmm_generate_complete_model_card.ipynb +++ b/examples/pzmm_generate_complete_model_card.ipynb @@ -578,7 +578,7 @@ " \"MartialStatus_Married_AF_spouse\", 'MartialStatus_Married_civ_spouse', 'MartialStatus_Never_married', 'MartialStatus_Divorced', 'MartialStatus_Separated', \n", " 'MartialStatus_Widowed', 'Race_White', 'Race_Black', 'Race_Asian_Pac_Islander', 'Race_Amer_Indian_Eskimo', 'Race_Other', 'Relationship_Husband', \n", " 'Relationship_Not_in_family', 'Relationship_Own_child', 'Relationship_Unmarried', 'Relationship_Wife', 'Relationship_Other_relative', 'WorkClass_Private',\n", - " 'Education_Bachelors'\n", + " 'Education_Bachelors', 'Education_Some_college', 'Education_HS_grad'\n", " ]\n", " # OHE columns must be removed after data combination\n", " predictor_columns = ['Age', 'HoursPerWeek', 'WorkClass_Private', 'WorkClass_Self', 'WorkClass_Gov', \n", diff --git a/src/sasctl/__init__.py b/src/sasctl/__init__.py index e4c7a0e1..18da61b8 100644 --- a/src/sasctl/__init__.py +++ b/src/sasctl/__init__.py @@ -4,7 +4,7 @@ # Copyright © 2019, SAS Institute Inc., Cary, NC, USA. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -__version__ = "1.11.2" +__version__ = "1.11.4" __author__ = "SAS" __credits__ = [ "Yi Jian Ching", diff --git a/src/sasctl/tasks.py b/src/sasctl/tasks.py index d466c10f..6bf5c67b 100644 --- a/src/sasctl/tasks.py +++ b/src/sasctl/tasks.py @@ -15,6 +15,7 @@ from pathlib import Path from typing import Union from warnings import warn +import zipfile import pandas as pd @@ -264,10 +265,9 @@ def _register_sas_model( out_var = [] in_var = [] import copy - import zipfile as zp zip_file_copy = copy.deepcopy(zip_file) - tmp_zip = zp.ZipFile(zip_file_copy) + tmp_zip = zipfile.ZipFile(zip_file_copy) if "outputVar.json" in tmp_zip.namelist(): out_var = json.loads( tmp_zip.read("outputVar.json").decode("utf=8") @@ -327,8 +327,8 @@ def _register_sas_model( if current_session().version_info() < 4: # Upload the model as a ZIP file if using Viya 3. - zipfile = utils.create_package(model, input=input) - model = mr.import_model_from_zip(name, project, zipfile, version=version) + zip_file = utils.create_package(model, input=input) + model = mr.import_model_from_zip(name, project, zip_file, version=version) else: # If using Viya 4, just upload the raw AStore and Model Manager will handle inspection. astore = cas.astore.download(rstore=model) @@ -998,3 +998,81 @@ def score_model_with_cas( print(score_execution_poll) score_results = se.get_score_execution_results(score_execution, use_cas_gateway) return score_results + + +def upload_local_model( + path: Union[str, Path], + model_name: str, + project_name: str, + repo_name: Union[str, dict] = None, + version: str = "latest", +): + """A function to upload a model and any associated files to the model repository. + Parameters + ---------- + path : Union[str, Path] + The path to the model and any associated files. + model_name : str + The name of the model. + project_name : str + The name of the project to which the model will be uploaded. + repo_name : Union[str, dict], optional + repository in which to create the project + version: str, optional + The version of the model being uploaded. Defaults to 'latest'. For new model version, use 'new'. + """ + # Use default repository if not specified + try: + if repo_name is None: + repository = mr.default_repository() + else: + repository = mr.get_repository(repo_name) + except HTTPError as e: + if e.code == 403: + raise AuthorizationError( + "Unable to register model. User account does not have read permissions " + "for the /modelRepository/repositories/ URL. Please contact your SAS " + "Viya administrator." + ) + raise e + + # Unable to find or create the repo. + if not repository and not repo_name: + raise ValueError("Unable to find a default repository") + if not repository: + raise ValueError(f"Unable to find repository '{repo_name}'") + + # Get project from repo if it exists; if it doesn't, create a new one + p = mr.get_project(project_name) + if p is None: + p = mr.create_project(project_name, repository) + + # zip up all files in directory (except any previous zip files) + zip_name = str(Path(path) / (model_name + ".zip")) + file_names = sorted(Path(path).glob("*[!(zip|sasast)]")) + sasast_file = next(Path(path).glob("*.sasast"), None) + if sasast_file: + # If a sasast file is present, upload it as well + with open(sasast_file, "rb") as sasast: + sasast_model = sasast.read() + data = { + "name": model_name, + "projectId": p.id, + "type": "ASTORE", + "versionOption": version, + } + files = {"files": (sasast_file.name, sasast_model)} + model = mr.post("/models", files=files, data=data) + for file in file_names: + with open(file, "r") as f: + mr.add_model_content(model, f, file.name) + else: + with zipfile.ZipFile(str(zip_name), mode="w") as zFile: + for file in file_names: + zFile.write(str(file), arcname=file.name) + # upload zipped model + with open(zip_name, "rb") as zip_file: + model = mr.import_model_from_zip( + model_name, project_name, zip_file, version=version + ) + return model