Skip to content

Commit 79cbc9f

Browse files
Merge pull request #1 from sassoftware/master
sync 1.4
2 parents acba636 + f8d54d8 commit 79cbc9f

10 files changed

+635
-1126
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,17 @@ Unreleased
33
----------
44
-
55

6+
v1.4 (2019-10-15)
7+
-----------------
8+
**Changes**
9+
- `PyMAS.score_code` now supports a `dest='Python'` option to retrieve the generated Python wrapper code.
10+
- `register_model` task includes a `python_wrapper.py` file when registering a Python model.
11+
- Improved error message when user lacks required permissions to register a model.
12+
13+
**Bugfixes**
14+
- Fixed an issue with CAS/EP score code that caused problems with model performance metrics.
15+
16+
617
v1.3 (2019-10-10)
718
-----------------
819
**Improvements**

src/sasctl/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Copyright © 2019, SAS Institute Inc., Cary, NC, USA. All Rights Reserved.
55
# SPDX-License-Identifier: Apache-2.0
66

7-
__version__ = '1.3'
7+
__version__ = '1.4'
88
__author__ = 'SAS'
99
__credits__ = ['Yi Jian Ching, Lucas De Paula, James Kochuba, Peter Tobac, '
1010
'Chris Toth, Jon Walker']

src/sasctl/exceptions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,10 @@ def __init__(self, username, *args, **kwargs):
1313
super(AuthenticationError, self).__init__(msg, *args, **kwargs)
1414

1515

16+
class AuthorizationError(RuntimeError):
17+
"""A user lacks permission to perform an action."""
18+
pass
19+
20+
1621
class JobTimeoutError(RuntimeError):
1722
pass

src/sasctl/tasks.py

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,11 @@
1515
import sys
1616
import warnings
1717

18+
from six.moves.urllib.error import HTTPError
19+
1820
from . import utils
1921
from .core import RestObj, current_session, get, get_link, request_link
22+
from .exceptions import AuthorizationError
2023
from .services import model_management as mm
2124
from .services import model_publish as mp
2225
from .services import model_repository as mr
@@ -180,10 +183,20 @@ def get_version(x):
180183
raise ValueError("Project '{}' not found".format(project))
181184

182185
# Use default repository if not specified
183-
if repository is None:
184-
repo_obj = mr.default_repository()
185-
else:
186-
repo_obj = mr.get_repository(repository)
186+
try:
187+
if repository is None:
188+
repo_obj = mr.default_repository()
189+
else:
190+
repo_obj = mr.get_repository(repository)
191+
except HTTPError as e:
192+
if e.code == 403:
193+
raise AuthorizationError('Unable to register model. User account '
194+
'does not have read permissions for the '
195+
'/modelRepository/repositories/ URL. '
196+
'Please contact your SAS Viya '
197+
'administrator.')
198+
else:
199+
raise e
187200

188201
# Unable to find or create the repo.
189202
if repo_obj is None and repository is None:
@@ -247,8 +260,8 @@ def get_version(x):
247260
files.append({'name': 'dmcas_epscorecode.sas',
248261
'file': mas_module.score_code(dest='CAS'),
249262
'role': 'score'})
250-
files.append({'name': 'python_wrappercode.py',
251-
'file': mas_module.score_code(dest='PY')})
263+
files.append({'name': 'python_wrapper.py',
264+
'file': mas_module.score_code(dest='Python')})
252265

253266
model['inputVariables'] = [var.as_model_metadata()
254267
for var in mas_module.variables

src/sasctl/utils/astore.py

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,6 @@ def create_files_from_astore(table):
131131
sess.loadactionset('astore')
132132

133133
result = sess.astore.describe(rstore=table, epcode=True)
134-
# astore = sess.astore.download(rstore=table)
135-
# if not hasattr(astore, "blob"):
136-
# raise ValueError("Failed to download binary data for ASTORE '%s'."
137-
# % astore)
138-
# astore = astore.blob
139-
#
140-
# astore = bytes(astore) # Convert from SWAT blob type
141134

142135
# Model Manager expects a 0-byte ASTORE file. Will retrieve actual ASTORE
143136
# from CAS during model publish.
@@ -149,11 +142,12 @@ def create_files_from_astore(table):
149142

150143
astore_key = result.Key.Key[0].strip()
151144

152-
# Remove "Keep" sas code from CAS/EP code so full table plus output are returned
153-
# This is so the MM performance charts and test work
154-
keepstart=result.epcode.find("Keep")
155-
keepend=result.epcode.find(";",keepstart)
145+
# Remove "Keep" sas code from CAS/EP code so full table plus output are
146+
# returned. This is so the MM performance charts and test work.
147+
keepstart = result.epcode.find("Keep")
148+
keepend = result.epcode.find(";", keepstart)
156149
ep_ds2 = result.epcode[0:keepstart] + result.epcode[keepend+1:]
150+
157151
package_ds2 = _generate_package_code(result)
158152
model_properties = _get_model_properties(result)
159153
input_vars = [get_variable_properties(var)

src/sasctl/utils/pymas/core.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -350,19 +350,21 @@ def score_code(self, input_table=None, output_table=None, columns=None, dest='MA
350350
The name of the table where execution results will be written
351351
columns : list of str
352352
Names of the columns from `table` that will be passed to `func`
353-
dest : str {'MAS', 'EP', 'CAS'}
353+
dest : str {'MAS', 'EP', 'CAS', 'Python'}
354+
Specifies the publishing destination for the score code to ensure
355+
that compatible code is generated.
354356
355357
Returns
356358
-------
359+
str
360+
Score code
357361
358-
"""
362+
.. versionchanged:: 1.3.1
363+
Added `dest='Python'` option
359364
365+
"""
360366
dest = dest.upper()
361367

362-
# Python code return
363-
if dest == 'PY':
364-
return '\n'.join(map(str, self.python_source))
365-
366368
# Check for names that could result in DS2 errors.
367369
DS2_KEYWORDS = ['input', 'output']
368370
for k in DS2_KEYWORDS:
@@ -394,4 +396,8 @@ def score_code(self, input_table=None, output_table=None, columns=None, dest='MA
394396
' end;',
395397
'enddata;')
396398

399+
elif dest == 'PYTHON':
400+
# Python code return
401+
code = self.package._python_code
402+
397403
return '\n'.join(code)

tests/cassettes/tests.integration.test_examples.test_sklearn_model.json

Lines changed: 218 additions & 953 deletions
Large diffs are not rendered by default.

tests/cassettes/tests.integration.test_tasks.TestModels.test_register_sklearn.json

Lines changed: 164 additions & 72 deletions
Large diffs are not rendered by default.

tests/cassettes/tests.integration.test_tasks.TestSklearnLinearModel.test_register_model.json

Lines changed: 167 additions & 75 deletions
Large diffs are not rendered by default.

tests/unit/test_tasks.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from six.moves import mock
99

1010
from sasctl.core import RestObj
11-
11+
from sasctl._services.model_repository import ModelRepository
1212

1313
def test_sklearn_metadata():
1414
pytest.importorskip('sklearn')
@@ -77,7 +77,7 @@ def test_parse_module_url():
7777
def test_save_performance_project_types():
7878
from sasctl.tasks import update_model_performance
7979

80-
with mock.patch('sasctl._services.model_repository.ModelRepository''.get_model') as model:
80+
with mock.patch('sasctl._services.model_repository.ModelRepository.get_model') as model:
8181
with mock.patch('sasctl._services.model_repository.ModelRepository.get_project') as project:
8282
model.return_value = RestObj(name='fakemodel', projectId=1)
8383

@@ -98,3 +98,34 @@ def test_save_performance_project_types():
9898
update_model_performance(None, None, None)
9999

100100
# Check projects w/ invalid properties
101+
102+
103+
@mock.patch.object(ModelRepository, 'get_repository')
104+
@mock.patch.object(ModelRepository, 'get_project')
105+
def test_register_model_403_error(get_project, get_repository):
106+
"""Verify HTTP 403 is converted to a user-friendly error.
107+
108+
Depending on environment configuration, this can happen when attempting to
109+
find a repository.
110+
111+
See: https://github.com/sassoftware/python-sasctl/issues/39
112+
"""
113+
114+
from six.moves.urllib.error import HTTPError
115+
from sasctl.exceptions import AuthorizationError
116+
from sasctl.tasks import register_model
117+
118+
get_project.return_value = {'name': 'Project Name'}
119+
get_repository.side_effect = HTTPError(None, 403, None, None, None)
120+
121+
# HTTP 403 error when getting repository should throw a user-friendly
122+
# AuthorizationError
123+
with pytest.raises(AuthorizationError):
124+
register_model(None, 'model name', 'project name')
125+
126+
# All other errors should be bubbled up
127+
get_repository.side_effect = HTTPError(None, 404, None, None, None)
128+
with pytest.raises(HTTPError):
129+
register_model(None, 'model name', 'project name')
130+
131+

0 commit comments

Comments
 (0)