Skip to content

Commit b291ca0

Browse files
Merge pull request #2 from sassoftware/master
sync to master repo
2 parents eef8b98 + 9ad3b8c commit b291ca0

14 files changed

+998
-579
lines changed

CHANGELOG.md

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

6+
v1.2.3 (2019-8-23)
7+
------------------
8+
**Changes**
9+
- `put` request will take an `item` parameter that's used to automatically populate headers for updates.
10+
11+
**Bugfixes**
12+
- Convert NaN values to null (None) when calling `microanalytic_score.execute_module_step`.
13+
14+
15+
v1.2.2 (2019-8-21)
16+
------------------
17+
**Bugfixes**
18+
- `register_model` task should now correctly identify columns when registering a Sci-kit pipeline.
19+
20+
21+
v1.2.1 (2019-8-20)
22+
------------------
23+
**Improvements**
24+
- Added the ability for `register_model` to correctly handle CAS tables containing data step
25+
score code.
26+
627

728
v1.2.0 (2019-8-16)
829
------------------

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ which can handle all necessary communication with the SAS Viya server:
8181
8282
>>> with Session('example.com', authinfo=<authinfo file>):
8383
... model = lm.LogisticRegression()
84-
... register_model('Sklearn Model', model, 'My Project')
84+
... register_model(model, 'Sklearn Model', 'My Project')
8585
```
8686

8787

@@ -165,7 +165,7 @@ Register a pure Python model in Model Manager:
165165
166166
>>> with Session(host, authinfo=<authinfo file>):
167167
... model = lm.LogisticRegression()
168-
... register_model('Sklearn Model', model, 'My Project')
168+
... register_model(model, 'Sklearn Model', 'My Project')
169169
```
170170

171171
Register a CAS model in Model Manager:

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.2.0'
7+
__version__ = '1.2.3'
88
__author__ = 'SAS'
99
__credits__ = ['Yi Jian Ching, Lucas De Paula, James Kochuba, Peter Tobac, '
1010
'Chris Toth, Jon Walker']

src/sasctl/_services/microanalytic_score.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import re
1010
from collections import OrderedDict
11+
from math import isnan
1112

1213
import six
1314

@@ -127,9 +128,17 @@ def execute_module_step(self, module, step, return_dict=True, **kwargs):
127128
elif type_name == 'int64':
128129
kwargs[k] = int(kwargs[k])
129130

130-
131131
body = {'inputs': [{'name': k, 'value': v}
132132
for k, v in six.iteritems(kwargs)]}
133+
134+
# Convert NaN to None (null) before calling MAS
135+
for input in body['inputs']:
136+
try:
137+
if isnan(input['value']):
138+
input['value'] = None
139+
except TypeError:
140+
pass
141+
133142
r = self.post('/modules/{}/steps/{}'.format(module, step), json=body)
134143

135144
# Convert list of name/value pair dictionaries to single dict

src/sasctl/core.py

Lines changed: 95 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -632,30 +632,115 @@ def is_uuid(id):
632632
return False
633633

634634

635-
def get(*args, **kwargs):
635+
def get(path, **kwargs):
636+
"""Send a GET request.
637+
638+
Parameters
639+
----------
640+
path : str
641+
The path portion of the URL.
642+
kwargs : any
643+
Passed to `request`.
644+
645+
Returns
646+
-------
647+
RestObj or None
648+
The results or None if the resource was not found.
649+
650+
"""
636651
try:
637-
return request('get', *args, **kwargs)
652+
return request('get', path, **kwargs)
638653
except HTTPError as e:
639654
if e.code == 404:
640655
return None # Resource not found
641656
else:
642657
raise e
643658

644659

645-
def head(*args, **kwargs):
646-
return request('head', *args, **kwargs)
660+
def head(path, **kwargs):
661+
"""Send a HEAD request.
662+
663+
Parameters
664+
----------
665+
path : str
666+
The path portion of the URL.
667+
kwargs : any
668+
Passed to `request`.
669+
670+
Returns
671+
-------
672+
RestObj
647673
674+
"""
675+
return request('head', path, **kwargs)
648676

649-
def post(*args, **kwargs):
650-
return request('post', *args, **kwargs)
651677

678+
def post(path, **kwargs):
679+
"""Send a POST request.
652680
653-
def put(*args, **kwargs):
654-
return request('put', *args, **kwargs)
681+
Parameters
682+
----------
683+
path : str
684+
The path portion of the URL.
685+
kwargs : any
686+
Passed to `request`.
655687
688+
Returns
689+
-------
690+
RestObj
656691
657-
def delete(*args, **kwargs):
658-
return request('delete', *args, **kwargs)
692+
"""
693+
return request('post', path, **kwargs)
694+
695+
696+
def put(path, item=None, **kwargs):
697+
"""Send a PUT request.
698+
699+
Parameters
700+
----------
701+
path : str
702+
The path portion of the URL.
703+
item : RestObj, optional
704+
A existing object to PUT. If provided, ETag and Content-Type headers
705+
will automatically be specified.
706+
kwargs : any
707+
Passed to `request`.
708+
709+
Returns
710+
-------
711+
RestObj
712+
713+
"""
714+
# If call is in the format put(url, RestObj), automatically fill in header
715+
# information
716+
if item is not None and isinstance(item, RestObj):
717+
get_headers = getattr(item, '_headers', None)
718+
if get_headers is not None:
719+
# Update the headers param if it was specified
720+
headers = kwargs.pop('headers', {})
721+
headers.setdefault('If-Match', get_headers.get('etag'))
722+
headers.setdefault('Content-Type', get_headers.get('content-type'))
723+
return request('put', path, json=item, headers=headers)
724+
725+
return request('put', path, **kwargs)
726+
727+
728+
def delete(path, **kwargs):
729+
"""Send a DELETE request.
730+
731+
Parameters
732+
----------
733+
path : str
734+
The path portion of the URL.
735+
kwargs : any
736+
Passed to `request`.
737+
738+
Returns
739+
-------
740+
RestObj
741+
742+
"""
743+
return request('delete', path, **kwargs)
659744

660745

661746
def request(verb, path, session=None, raw=False, **kwargs):

src/sasctl/tasks.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ def get_version(x):
187187
# If model is a CASTable then assume it holds an ASTORE model.
188188
# Import these via a ZIP file.
189189
if 'swat.cas.table.CASTable' in str(type(model)):
190-
zipfile = utils.create_package_from_astore(model)
190+
zipfile = utils.create_package(model)
191191

192192
if create_project:
193193
project = mr.create_project(project, repository)

src/sasctl/utils/__init__.py

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

7-
from .astore import create_package_from_astore
7+
from .astore import create_package, create_package_from_astore
88

99

src/sasctl/utils/astore.py

Lines changed: 105 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,77 @@
1212
import uuid
1313
import zipfile
1414

15+
import six
16+
1517
try:
1618
import swat
1719
except ImportError:
1820
swat = None
1921

2022

23+
def create_package(table):
24+
"""Create an importable model package from a CAS table.
25+
26+
Parameters
27+
----------
28+
table : swat.CASTable
29+
The CAS table containing an ASTORE or score code.
30+
31+
Returns
32+
-------
33+
BytesIO
34+
A byte stream representing a ZIP archive which can be imported.
35+
36+
See Also
37+
--------
38+
:meth:`model_repository.import_model_from_zip <.ModelRepository.import_model_from_zip>`
39+
40+
"""
41+
if swat is None:
42+
raise RuntimeError(
43+
"The 'swat' package is required to work with SAS models.")
44+
45+
assert isinstance(table, swat.CASTable)
46+
47+
if 'DataStepSrc' in table.columns:
48+
return create_package_from_datastep(table)
49+
else:
50+
return create_package_from_astore(table)
51+
52+
53+
def create_package_from_datastep(table):
54+
"""Create an importable model package from a score code table.
55+
56+
Parameters
57+
----------
58+
table : swat.CASTable
59+
The CAS table containing the score code.
60+
61+
Returns
62+
-------
63+
BytesIO
64+
A byte stream representing a ZIP archive which can be imported.
65+
66+
See Also
67+
--------
68+
:meth:`model_repository.import_model_from_zip <.ModelRepository.import_model_from_zip>`
69+
70+
"""
71+
assert 'DataStepSrc' in table.columns
72+
sess = table.session.get_connection()
73+
74+
dscode = table.to_frame().loc[0, 'DataStepSrc']
75+
76+
file_metadata = [{'role': 'score', 'name': 'dmcas_scorecode.sas'}]
77+
78+
zip_file = _build_zip_from_files({
79+
'fileMetadata.json': file_metadata,
80+
'dmcas_scorecode.sas': dscode
81+
})
82+
83+
return zip_file
84+
85+
2186
def create_package_from_astore(table):
2287
"""Create an importable model package from an ASTORE.
2388
@@ -81,45 +146,62 @@ def create_package_from_astore(table):
81146
'uri': '/dataTables/dataSources/cas~fs~cas-shared-default~fs~ModelStore/tables/{}'.format(astore_filename),
82147
'key': astore_key}]
83148

149+
zip_file = _build_zip_from_files({
150+
'dmcas_packagescorecode.sas': '\n'.join(package_ds2),
151+
'dmcas_epscorecode.sas': ep_ds2,
152+
astore_filename: astore,
153+
'ModelProperties.json': model_properties,
154+
'fileMetadata.json': file_metadata,
155+
'AstoreMetadata.json': astore_metadata,
156+
'inputVar.json': input_vars,
157+
'outputVar.json': output_vars
158+
})
159+
160+
return zip_file
161+
162+
163+
def _build_zip_from_files(files):
164+
"""Create a ZIP file containing the provided files.
165+
166+
Parameters
167+
----------
168+
files : dict
169+
Dictionary of filename: content to be added to the .zip file.
170+
171+
Returns
172+
-------
173+
BytesIO
174+
Byte stream representation of the .zip file.
175+
176+
"""
84177
try:
85178
# Create a temp folder
86179
folder = tempfile.mkdtemp()
87180

88-
# Closure for easily adding JSON files
89-
def json_file(data, filename):
90-
filename = os.path.join(folder, filename)
91-
with open(filename, 'w') as f:
92-
json.dump(data, f, indent=1)
181+
for k, v in six.iteritems(files):
182+
filename = os.path.join(folder, k)
93183

94-
filename = os.path.join(folder, 'dmcas_packagescorecode.sas')
95-
with open(filename, 'w') as f:
96-
f.write('\n'.join(package_ds2))
184+
# Write JSON file
185+
if os.path.splitext(k)[-1].lower() == '.json':
186+
with open(filename, 'w') as f:
187+
json.dump(v, f, indent=1)
188+
else:
189+
mode = 'wb' if isinstance(v, bytes) else 'w'
97190

98-
filename = os.path.join(folder, 'dmcas_epscorecode.sas')
99-
with open(filename, 'w') as f:
100-
f.write(ep_ds2)
101-
102-
filename = os.path.join(folder, astore_filename)
103-
with open(filename, 'wb') as f:
104-
f.write(astore)
105-
106-
json_file(model_properties, 'ModelProperties.json')
107-
json_file(file_metadata, 'fileMetadata.json')
108-
json_file(astore_metadata, 'AstoreMetadata.json')
109-
json_file(input_vars, 'inputVar.json')
110-
json_file(output_vars, 'outputVar.json')
191+
with open(filename, mode) as f:
192+
f.write(v)
111193

112194
files = os.listdir(folder)
113195

114196
with zipfile.ZipFile(os.path.join(folder, 'model.zip'), 'w') as z:
115197
for file in files:
116198
z.write(os.path.join(folder, file), file)
117199

118-
# Need to return the ZIP file data but also need to ensure the directory is cleaned up.
200+
# Need to return the ZIP file data but also need to ensure the
201+
# directory is cleaned up.
119202
# Read the bytes from disk and return an in memory "file".
120203
with open(os.path.join(folder, 'model.zip'), 'rb') as z:
121204
return io.BytesIO(z.read())
122-
123205
finally:
124206
shutil.rmtree(folder)
125207

src/sasctl/utils/pymas/core.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,6 @@ def _build_pymas(obj, func_name=None, input_types=None, array_input=False,
276276
# Run one observation through the model and use the result to
277277
# determine output variables
278278
output = target_func(input_types.head(1))
279-
# output = target_func(input_types.iloc[0, :].values.reshape((1, -1)))
280279
output_vars = ds2_variables(output, output_vars=True)
281280
vars.extend(output_vars)
282281
elif isinstance(input_types, type):

0 commit comments

Comments
 (0)