├── swat ├── tests │ ├── __init__.py │ ├── cas │ │ ├── __init__.py │ │ ├── test_csess.py │ │ ├── test_datapreprocess.py │ │ ├── test_casa.py │ │ ├── test_echo.py │ │ ├── test_response.py │ │ └── test_datetime.py │ ├── datasources │ │ ├── iicc.csv │ │ ├── cars.xls │ │ ├── cars.sas7bdat │ │ ├── dates.csv │ │ ├── cars_single.sashdat │ │ ├── summary_array.sashdat │ │ ├── datetimes.csv │ │ ├── datetime.csv │ │ ├── merge_company.csv │ │ ├── merge_finance.csv │ │ └── merge_repertory.csv │ ├── test_types.py │ ├── test_keyword.py │ ├── test_decorators.py │ └── test_formatter.py ├── readme.md ├── lib │ ├── linux │ │ └── __init__.py │ ├── mac │ │ └── __init__.py │ ├── win │ │ └── __init__.py │ └── __init__.py ├── magics.py ├── render │ ├── __init__.py │ ├── generic.py │ └── html.py ├── datamsghandlers.py ├── cas │ ├── utils │ │ ├── __init__.py │ │ ├── tk.py │ │ └── misc.py │ ├── rest │ │ ├── __init__.py │ │ ├── error.py │ │ ├── message.py │ │ └── value.py │ ├── __init__.py │ ├── types.py │ ├── request.py │ └── magics.py ├── utils │ ├── __init__.py │ ├── json.py │ ├── keyword.py │ ├── testingmocks.py │ ├── decorators.py │ ├── datetime.py │ ├── compat.py │ ├── authinfo.py │ └── args.py ├── logging.py ├── exceptions.py ├── notebook │ ├── __init__.py │ └── zeppelin.py ├── __init__.py └── functions.py ├── doc ├── source │ ├── _static │ │ ├── custom.css │ │ ├── rest-workflow.png │ │ ├── binary-workflow.png │ │ ├── easyway-workflow.png │ │ ├── callbacks-workflow.png │ │ └── simultaneous-workflow.png │ ├── sas-tk.rst │ ├── sorting.rst │ ├── index.rst │ ├── install.rst │ ├── encryption.rst │ ├── binary-vs-rest.rst │ ├── table-vs-dataframe.rst │ ├── bygroups.rst │ └── licenses.rst ├── check-doc-errors └── post-process-sphinx-html ├── SUPPORT.md ├── .gitattributes ├── .github └── workflows │ └── python-linting.yml ├── cicd ├── get-workspace-url.py ├── get-protocol.py ├── get-version.py ├── get-host.py ├── download-wheels.py ├── get-basename.py ├── upload-assests.py ├── get-server-info.py └── generate-tox-ini.py ├── .gitignore ├── setup.py ├── ContributorAgreement.txt ├── conda.recipe └── meta.yaml ├── CONTRIBUTING.md ├── tox.ini └── CHANGELOG.md /swat/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /swat/tests/cas/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /swat/tests/datasources/iicc.csv: -------------------------------------------------------------------------------- 1 | C1,C2 2 | 1,2 3 | 2,4 4 | 3,6 5 | -------------------------------------------------------------------------------- /doc/source/_static/custom.css: -------------------------------------------------------------------------------- 1 | code.docutils > .pre { color: #505050 !important } 2 | -------------------------------------------------------------------------------- /swat/tests/datasources/cars.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sassoftware/python-swat/HEAD/swat/tests/datasources/cars.xls -------------------------------------------------------------------------------- /doc/source/_static/rest-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sassoftware/python-swat/HEAD/doc/source/_static/rest-workflow.png -------------------------------------------------------------------------------- /swat/tests/datasources/cars.sas7bdat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sassoftware/python-swat/HEAD/swat/tests/datasources/cars.sas7bdat -------------------------------------------------------------------------------- /swat/tests/datasources/dates.csv: -------------------------------------------------------------------------------- 1 | Region,Date,Label 2 | N,20,21JAN60 3 | S,30,31JAN60 4 | E,300,27OCT60 5 | W,400,04FEB61 6 | X,., 7 | -------------------------------------------------------------------------------- /doc/source/_static/binary-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sassoftware/python-swat/HEAD/doc/source/_static/binary-workflow.png -------------------------------------------------------------------------------- /doc/source/_static/easyway-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sassoftware/python-swat/HEAD/doc/source/_static/easyway-workflow.png -------------------------------------------------------------------------------- /doc/source/_static/callbacks-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sassoftware/python-swat/HEAD/doc/source/_static/callbacks-workflow.png -------------------------------------------------------------------------------- /swat/tests/datasources/cars_single.sashdat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sassoftware/python-swat/HEAD/swat/tests/datasources/cars_single.sashdat -------------------------------------------------------------------------------- /SUPPORT.md: -------------------------------------------------------------------------------- 1 | ## Support 2 | 3 | We use GitHub for tracking bugs and feature requests. Please submit a GitHub issue or pull request for support. 4 | -------------------------------------------------------------------------------- /doc/source/_static/simultaneous-workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sassoftware/python-swat/HEAD/doc/source/_static/simultaneous-workflow.png -------------------------------------------------------------------------------- /swat/tests/datasources/summary_array.sashdat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sassoftware/python-swat/HEAD/swat/tests/datasources/summary_array.sashdat -------------------------------------------------------------------------------- /swat/tests/datasources/datetimes.csv: -------------------------------------------------------------------------------- 1 | Region,Datetime,Label 2 | N,1268870400,03MAR2000:00:00:00 3 | S,1003329932,17OCT1991:14:45:32 4 | E,0,01JAN01:00:00:00 5 | W,-1,12DEC31:12:59:59 6 | X,., 7 | -------------------------------------------------------------------------------- /swat/tests/datasources/datetime.csv: -------------------------------------------------------------------------------- 1 | date,time,datetime 2 | 2015/01/01,12:12:59.345,20150101T12:12:59.345 3 | 2012/04/12,05:49:23,20120412T05:49:23 4 | 1984/10/08,14:14:34.2,1984/10/08T14:14:34.2 5 | 1950/03/04,18:00:00,1950/03/04T18:00:00 6 | -------------------------------------------------------------------------------- /swat/tests/datasources/merge_company.csv: -------------------------------------------------------------------------------- 1 | Name,Age,Gender 2 | "Benito, Gisela",32,F 3 | "Gunter, Thomas",27,M 4 | "Harbinger, Nicholas",36,M 5 | "Morrison, Michael",32,M 6 | "Phillipon, Marie-Odile",28,F 7 | "Rudelich, Herbert",39,M 8 | "Sirignano, Emily",12,F 9 | "Vincent, Martina",34,F 10 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | # enforce text on certain files 3 | *.py text 4 | *.pyx text 5 | *.pyd text 6 | *.c text 7 | *.h text 8 | *.html text 9 | *.csv text 10 | *.json text 11 | *.pickle binary 12 | *.h5 binary 13 | *.dta binary 14 | *.xls binary 15 | *.xlsx binary 16 | *.bat text eol=crlf 17 | *.html text 18 | -------------------------------------------------------------------------------- /swat/tests/datasources/merge_finance.csv: -------------------------------------------------------------------------------- 1 | IdNumber,Name,Salary,FinanceId 2 | 228-88-9649,"Benito, Gisela",28000,228-88-9649 3 | 929-75-0218,"Gunter, Thomas",27500,929-75-0218 4 | 446-93-2122,"Harbinger, Nicholas",33900,446-93-2122 5 | 776-84-5391,"Phillipon, Marie-Odile",29750,776-84-5391 6 | 029-46-9261,"Rudelich, Herbert",35000,029-46-9261 7 | 000-47-8262,"Smith, Joe",30000,000-47-8262 8 | -------------------------------------------------------------------------------- /swat/readme.md: -------------------------------------------------------------------------------- 1 | For **Python 3.12+ on Windows only**, the following modification was made to the `pyport.h` file while building the SWAT C extensions: 2 | 3 | * Updated the `#define` for `ALWAYS_INLINE` 4 |
**Previous Definition :** 5 | ```c 6 | #elif defined(__GNUC__) || defined(__clang__) || defined(__INTEL_COMPILER) 7 | ``` 8 | **Updated Definition :** 9 | ```c 10 | #elif defined(__GNUC__) || defined(__clang__) || defined(__INTEL_LLVM_COMPILER) || (defined(__INTEL_COMPILER) && !defined(_WIN32)) 11 | ``` 12 | 13 | This change addresses a compiler error encountered when using the Intel compiler on Windows. 14 | -------------------------------------------------------------------------------- /doc/check-doc-errors: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Check the generated HTML for CAS / ESP connection errors 5 | 6 | ''' 7 | 8 | import glob 9 | import os 10 | import sys 11 | 12 | if '-h' in sys.argv or '--help' in sys.argv or len(sys.argv) != 2: 13 | print('usage: %s root' % os.path.basename(sys.argv[0])) 14 | sys.exit(1) 15 | 16 | root = sys.argv[1] 17 | 18 | for html in glob.glob(os.path.join(root, '*.html')): 19 | with open(html, 'r') as html_file: 20 | txt = html_file.read() 21 | if 'Traceback (most recent call last)' in txt: 22 | sys.stderr.write("ERROR: Found unexpected exception in '%s'\n" % html) 23 | sys.exit(1) 24 | -------------------------------------------------------------------------------- /swat/lib/linux/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | SWAT TK Linux Libraries 21 | 22 | ''' 23 | -------------------------------------------------------------------------------- /swat/lib/mac/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | SWAT TK Macintosh Libraries 21 | 22 | ''' 23 | -------------------------------------------------------------------------------- /swat/lib/win/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | SWAT TK Windows Libraries 21 | 22 | ''' 23 | -------------------------------------------------------------------------------- /swat/lib/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | SWAT TK Platform-specific Libraries 21 | 22 | ''' 23 | -------------------------------------------------------------------------------- /swat/tests/datasources/merge_repertory.csv: -------------------------------------------------------------------------------- 1 | Play,Role,IdNumber,RepId 2 | No Exit,Estelle,074-53-9892,074-53-9892 3 | No Exit,Inez,776-84-5391,776-84-5391 4 | No Exit,Valet,929-75-0218,929-75-0218 5 | No Exit,Garcin,446-93-2122,446-93-2122 6 | The Glass Menagerie,Amanda Wingfield,228-88-9649,228-88-9649 7 | The Glass Menagerie,Laura Wingfield,776-84-5391,776-84-5391 8 | The Glass Menagerie,Tom Wingfield,929-75-0218,929-75-0218 9 | The Glass Menagerie,Jim O'Connor,029-46-9261,029-46-9261 10 | The Dear Departed,Mrs. Slater,228-88-9649,228-88-9649 11 | The Dear Departed,Mrs. Jordan,074-53-9892,074-53-9892 12 | The Dear Departed,Henry Slater,029-46-9261,029-46-9261 13 | The Dear Departed,Ben Jordan,446-93-2122,446-93-2122 14 | The Dear Departed,Victoria Slater,442-21-8075,442-21-8075 15 | The Dear Departed,Abel Merryweather,929-75-0218,929-75-0218 16 | -------------------------------------------------------------------------------- /swat/magics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | IPython magics 21 | 22 | (Only here for backwards compatibility) 23 | 24 | ''' 25 | 26 | from __future__ import print_function, division, absolute_import, unicode_literals 27 | 28 | from .cas.magics import * # noqa: F403 29 | -------------------------------------------------------------------------------- /.github/workflows/python-linting.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python linting 5 | 6 | on: [push, pull_request] 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | python-version: [3.9.22] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Set up Python ${{ matrix.python-version }} 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | python -m pip install --upgrade pip 25 | pip install flake8 26 | - name: Lint with flake8 27 | run: | 28 | flake8 . --config tox.ini --show-source --statistics 29 | -------------------------------------------------------------------------------- /swat/render/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | Rendering functions for creating ODS-like output 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | from .generic import render 27 | from .html import render_html 28 | -------------------------------------------------------------------------------- /swat/datamsghandlers.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | Data message handlers 21 | 22 | (Only here for backwards compatibility) 23 | 24 | ''' 25 | 26 | from __future__ import print_function, division, absolute_import, unicode_literals 27 | 28 | from .cas.datamsghandlers import * # noqa: F403 29 | -------------------------------------------------------------------------------- /swat/cas/utils/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | Utilities for CAS modules 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | from .misc import super_dir 27 | from .params import vl, table 28 | from .tk import InitializeTK, initialize_tk 29 | -------------------------------------------------------------------------------- /swat/render/generic.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | Generic rendering functions 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | from .html import render_html 27 | 28 | 29 | def render(results): 30 | ''' Render the object using appropriate rendering function ''' 31 | render_html(results) 32 | -------------------------------------------------------------------------------- /swat/cas/rest/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | Class for creating CAS sessions 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | from .connection import REST_CASConnection 27 | from .error import REST_CASError 28 | from .message import REST_CASMessage 29 | from .response import REST_CASResponse 30 | from .table import REST_CASTable 31 | from .value import REST_CASValue 32 | -------------------------------------------------------------------------------- /cicd/get-workspace-url.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Return the WORKSPACE as a URL 5 | 6 | Some downstream commands require the path to a Jenkins `WORKSPACE` variable 7 | as a URL rather than a file path. This URL must be formatted differently for 8 | Windows than for UNIX-like operating systems. This utility does the proper 9 | formatting for the host type. 10 | 11 | ''' 12 | 13 | import argparse 14 | import itertools 15 | import os 16 | import re 17 | import sys 18 | 19 | 20 | def main(args): 21 | ''' Main routine ''' 22 | cwd = os.getcwd() 23 | 24 | if sys.platform.lower().startswith('win'): 25 | workspace_url = 'file:///{}'.format(re.sub(r'^(/[A-Za-z])/', '\1:/', 26 | cwd.replace('\\', '/'))) 27 | else: 28 | workspace_url = 'file://{}'.format(cwd) 29 | 30 | print(workspace_url) 31 | 32 | 33 | if __name__ == '__main__': 34 | parser = argparse.ArgumentParser(description=__doc__.strip(), 35 | formatter_class=argparse.RawTextHelpFormatter) 36 | 37 | args = parser.parse_args() 38 | 39 | main(args) 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Editor temporary/working/backup files 2 | .#* 3 | *\#*\# 4 | [#]*# 5 | *~ 6 | *$ 7 | *.bak 8 | *.old 9 | *flymake* 10 | *.kdev4 11 | *.log 12 | *.nfs* 13 | *.swo 14 | *.swp 15 | *.pdb 16 | .project 17 | .pydevproject 18 | .settings 19 | .idea 20 | .vagrant 21 | .noseids 22 | .ipynb_checkpoints 23 | .tags 24 | .vscode 25 | 26 | ## Compiled source 27 | *.a 28 | *.com 29 | *.class 30 | *.dll 31 | *.dylib 32 | *.exe 33 | *.o 34 | *.py[ocd] 35 | *.so 36 | *.na 37 | .build_cache_dir 38 | MANIFEST 39 | 40 | ## Python files 41 | # setup.py working directory 42 | build 43 | # sphinx build directory 44 | doc/_build 45 | # setup.py dist directory 46 | dist 47 | # Egg metadata 48 | *.egg-info 49 | .eggs 50 | .pypirc 51 | 52 | ## tox testing tool 53 | .tox 54 | # rope 55 | .ropeproject 56 | # wheel files 57 | *.whl 58 | **/wheelhouse/* 59 | # coverage 60 | .coverage 61 | swat.egg-info/ 62 | __pycache__/ 63 | _stats.txt 64 | cover/ 65 | test-reports/ 66 | 67 | ## OS generated files 68 | .directory 69 | .gdb_history 70 | .DS_Store 71 | ehthumbs.db 72 | Icon? 73 | Thumbs.db 74 | 75 | ## Documentation generated files 76 | doc/build 77 | doc/source/generated 78 | -------------------------------------------------------------------------------- /swat/utils/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | General utilities for the SWAT module 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | from . import compat 27 | from . import config 28 | from .decorators import cachedproperty, getattr_safe_property 29 | from .args import mergedefined, dict2kwargs, getsoptions, getlocale, parsesoptions 30 | from .json import escapejson 31 | from .keyword import dekeywordify, keywordify 32 | -------------------------------------------------------------------------------- /swat/tests/test_types.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import swat 20 | import swat.utils.testing as tm 21 | import unittest 22 | 23 | 24 | class TestTypes(tm.TestCase): 25 | 26 | def test_str(self): 27 | nil = swat.nil 28 | 29 | self.assertTrue(isinstance(str(nil), str)) 30 | self.assertEqual(str(nil), 'nil') 31 | 32 | self.assertTrue(isinstance(repr(nil), str)) 33 | self.assertEqual(repr(nil), 'nil') 34 | 35 | 36 | if __name__ == '__main__': 37 | tm.runtests() 38 | -------------------------------------------------------------------------------- /swat/cas/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | Classes and functions for interfacing with CAS 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | from .utils import InitializeTK, vl, table, initialize_tk 27 | from .actions import CASAction, CASActionSet 28 | from .connection import CAS, getone, getnext, dir_actions, dir_members 29 | from .table import CASTable 30 | from .transformers import py2cas 31 | from .types import nil, blob 32 | from .request import CASRequest 33 | from .response import CASResponse 34 | from .results import CASResults 35 | -------------------------------------------------------------------------------- /doc/source/sas-tk.rst: -------------------------------------------------------------------------------- 1 | 2 | .. Copyright SAS Institute 3 | 4 | .. _sastk: 5 | 6 | ****** 7 | SAS TK 8 | ****** 9 | 10 | The SAS TK subsystem is a set of components used for various operations in 11 | SAS products. It can be thought of as SAS' standard library. The binary 12 | communication used by CAS is included in the functionality of these components. 13 | 14 | In order to support binary communications with CAS in SWAT, a subset of 15 | the shared objects (or DLLs) is included in the SWAT install file for 16 | supported platforms along with a Python extension module that interfaces with 17 | them. 18 | 19 | In addition to communication with the CAS server, the TK components also 20 | contain functionality for applying SAS data formats to Python values for 21 | rendering purposes. This includes only the standard SAS data formats, not 22 | user-defined formats. 23 | 24 | If you plan to use the binary protocol of CAS, the SAS TK components are 25 | required. The REST interface can be accessed using SWAT's Python code. 26 | For more information on protocol comparisons, see the 27 | :ref:`Binary vs. REST ` section. 28 | 29 | The SAS TK components are also released under a separate license from the 30 | Python source. See :ref:`Licenses ` for more information. 31 | -------------------------------------------------------------------------------- /swat/tests/test_keyword.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import swat.utils.testing as tm 20 | import unittest 21 | from swat.utils.keyword import dekeywordify, keywordify 22 | 23 | 24 | class TestKeyword(tm.TestCase): 25 | 26 | def test_dekeywordify(self): 27 | self.assertEqual(dekeywordify('from'), 'from_') 28 | self.assertEqual(dekeywordify('to'), 'to') 29 | self.assertEqual(dekeywordify(10), 10) 30 | 31 | def test_keywordify(self): 32 | self.assertEqual(keywordify('from_'), 'from') 33 | self.assertEqual(keywordify('to'), 'to') 34 | self.assertEqual(keywordify(10), 10) 35 | 36 | 37 | if __name__ == '__main__': 38 | tm.runtests() 39 | -------------------------------------------------------------------------------- /swat/logging.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | SWAT logging functions 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | import logging 27 | import sys 28 | 29 | default_level = 'warning' 30 | default_format = '[%(levelname)s] %(message)s' 31 | 32 | # Create global logger 33 | logger = logging.getLogger(__name__) 34 | logger.setLevel(dict( 35 | debug=logging.DEBUG, 36 | info=logging.INFO, 37 | warning=logging.WARNING, 38 | error=logging.ERROR, 39 | critical=logging.CRITICAL, 40 | )[default_level]) 41 | handler = logging.StreamHandler(sys.stderr) 42 | handler.setFormatter(logging.Formatter(default_format)) 43 | logger.addHandler(handler) 44 | -------------------------------------------------------------------------------- /swat/cas/utils/tk.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | TK utilities for interfacing with CAS 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | from ... import clib 27 | from ...utils.compat import a2n 28 | 29 | 30 | def InitializeTK(path): 31 | ''' 32 | Initialize the TK subsystem 33 | 34 | Parameters 35 | ---------- 36 | path : string 37 | Colon (semicolon on Windows) separated list of directories to 38 | search for TK components 39 | 40 | 41 | Examples 42 | -------- 43 | Set the TK search path to look through /usr/local/tk/ and /opt/sas/tk/. 44 | 45 | >>> swat.InitializeTK('/usr/local/tk:/opt/sas/tk') 46 | 47 | ''' 48 | clib.InitializeTK(a2n(path, 'utf-8')) 49 | 50 | 51 | initialize_tk = InitializeTK 52 | -------------------------------------------------------------------------------- /swat/tests/test_decorators.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import swat.utils.testing as tm 20 | import unittest 21 | from swat.utils.decorators import cachedproperty 22 | 23 | 24 | class TestKeyword(tm.TestCase): 25 | 26 | def test_cachedproperty(self): 27 | 28 | class MyClass(object): 29 | 30 | @cachedproperty 31 | def cprop(self): 32 | return {'key': 'value'} 33 | 34 | @property 35 | def prop(self): 36 | return {'key': 'value'} 37 | 38 | mycls = MyClass() 39 | 40 | self.assertTrue(mycls.prop is not mycls.prop) 41 | self.assertEqual(mycls.prop, {'key': 'value'}) 42 | 43 | self.assertTrue(mycls.cprop is mycls.cprop) 44 | self.assertEqual(mycls.prop, {'key': 'value'}) 45 | 46 | 47 | if __name__ == '__main__': 48 | tm.runtests() 49 | -------------------------------------------------------------------------------- /swat/cas/types.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | Additional types for CAS support 21 | 22 | ''' 23 | 24 | from __future__ import (print_function, division, absolute_import, 25 | unicode_literals) 26 | 27 | import six 28 | 29 | 30 | @six.python_2_unicode_compatible 31 | class NilType(object): 32 | ''' 33 | Type for `nil` valued parameters 34 | 35 | The swat module contains a singleton of the NilType class called `nil`. 36 | 37 | Examples 38 | -------- 39 | Send a nil as a parameter value 40 | 41 | >>> s.action(param=swat.nil) 42 | 43 | ''' 44 | 45 | def __repr__(self): 46 | return str(self) 47 | 48 | def __str__(self): 49 | return 'nil' 50 | 51 | 52 | # nil singleton 53 | nil = NilType() 54 | 55 | 56 | class blob(bytes): 57 | ''' 58 | Explicitly defined type for blob parameters 59 | 60 | ''' 61 | -------------------------------------------------------------------------------- /cicd/get-protocol.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Return the preferred CAS protocol ('cas' if TK is available; 'http' otherwise) 5 | 6 | This utility checks the package `__init__.py` file for a `__tk_version__` 7 | parameter that indicates the packaged version of the TK libraries. 8 | If it finds it, the `cas` protocol will be returned. If the value is 9 | set to `None`, the `http` protocol is returned. 10 | 11 | ''' 12 | 13 | import argparse 14 | import glob 15 | import os 16 | import re 17 | import sys 18 | 19 | 20 | def main(args): 21 | ''' Main routine ''' 22 | version = None 23 | 24 | try: 25 | init = glob.glob(os.path.join(args.root, 'swat', '__init__.py'))[0] 26 | except IndexError: 27 | sys.stderr.write('ERROR: Could not locate swat/__init__.py file\n') 28 | sys.exit(1) 29 | 30 | with open(init, 'r') as init_in: 31 | for line in init_in: 32 | m = re.match(r'''__tk_version__\s*=\s*['"]([^'"]+)['"]''', line) 33 | if m: 34 | version = m.group(1) 35 | if version == 'none': 36 | version = None 37 | break 38 | 39 | print(version and 'cas' or 'http') 40 | 41 | 42 | if __name__ == '__main__': 43 | parser = argparse.ArgumentParser(description=__doc__.strip(), 44 | formatter_class=argparse.RawTextHelpFormatter) 45 | 46 | parser.add_argument('root', type=str, metavar='', 47 | default='.', nargs='?', 48 | help='root directory of Python package') 49 | 50 | args = parser.parse_args() 51 | 52 | sys.exit(main(args) or 0) 53 | -------------------------------------------------------------------------------- /swat/cas/request.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | Utilities for dealing with requests from a CAS action 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | from ..clib import errorcheck 27 | from .transformers import casvaluelist2py 28 | 29 | 30 | class CASRequest(object): 31 | ''' 32 | Create a CASRequest object 33 | 34 | Parameters 35 | ---------- 36 | _sw_request : SWIG CASRequest object 37 | The SWIG request object 38 | 39 | soptions : string, optional 40 | soptions of the connection object 41 | 42 | Returns 43 | ------- 44 | CASRequest object 45 | 46 | ''' 47 | 48 | def __init__(self, _sw_request, soptions=''): 49 | self._sw_request = _sw_request 50 | self._soptions = soptions 51 | 52 | nparams = errorcheck(_sw_request.getNParameters(), _sw_request) 53 | params = errorcheck(_sw_request.getParameters(), _sw_request) 54 | 55 | self.parameters = casvaluelist2py(params, self._soptions, nparams) 56 | -------------------------------------------------------------------------------- /swat/utils/json.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | General JSON processing utilities 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | 27 | def escapejson(jsonstr): 28 | ''' 29 | Escape quotes in JSON strings 30 | 31 | Parameters 32 | ---------- 33 | jsonstr : JSON string 34 | 35 | Returns 36 | ------- 37 | string 38 | String with quotes and newlines escaped 39 | 40 | ''' 41 | jsonstr = jsonstr.replace('\\', '\\\\') 42 | jsonstr = jsonstr.replace(r'\"', r'\\"') 43 | # jsonstr = jsonstr.replace(r'\b', r'\\b') 44 | # jsonstr = jsonstr.replace(r'\f', r'\\f') 45 | # jsonstr = jsonstr.replace(r'\n', r'\\n') 46 | # jsonstr = jsonstr.replace(r'\r', r'\\r') 47 | # jsonstr = jsonstr.replace(r'\t', r'\\t') 48 | jsonstr = jsonstr.replace('\b', r'\u0008') 49 | jsonstr = jsonstr.replace('\f', r'\u000C') 50 | jsonstr = jsonstr.replace('\n', r'\u000A') 51 | jsonstr = jsonstr.replace('\r', r'\u000D') 52 | jsonstr = jsonstr.replace('\t', r'\u0009') 53 | return jsonstr 54 | -------------------------------------------------------------------------------- /swat/cas/rest/error.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | Class for CAS errors 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | 27 | class REST_CASError(object): 28 | ''' 29 | Create a CASError object 30 | 31 | Parameters 32 | ---------- 33 | soptions : string 34 | Instantiation options 35 | 36 | Returns 37 | ------- 38 | REST_CASError 39 | 40 | ''' 41 | 42 | def __init__(self, soptions=''): 43 | self._soptions = soptions 44 | self._message = '' 45 | 46 | def getTypeName(self): 47 | ''' Get object type ''' 48 | return 'error' 49 | 50 | def getSOptions(self): 51 | ''' Get SOptions value ''' 52 | return self._soptions 53 | 54 | def isNULL(self): 55 | ''' Is this a NULL object? ''' 56 | return False 57 | 58 | def getLastErrorMessage(self): 59 | ''' Get the last generated error message ''' 60 | return self._message 61 | 62 | def setErrorMessage(self, msg): 63 | ''' Set the last generated error message ''' 64 | self._message = msg 65 | -------------------------------------------------------------------------------- /swat/utils/keyword.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | General utilities for dealing with keywords 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | import keyword 27 | 28 | DEKEYWORDS = set([(x + '_') for x in keyword.kwlist]) 29 | 30 | 31 | def dekeywordify(name): 32 | ''' 33 | Add an underscore to names that are keywords 34 | 35 | Parameters 36 | ---------- 37 | name : string 38 | The string to check against keywords 39 | 40 | Returns 41 | ------- 42 | string 43 | Name changed to avoid keywords 44 | 45 | ''' 46 | if keyword.iskeyword(name): 47 | return name + '_' 48 | return name 49 | 50 | 51 | def keywordify(name): 52 | ''' 53 | Convert name that has been dekeywordified to a keyword 54 | 55 | Parameters 56 | ---------- 57 | name : string 58 | The string to convert to a keyword if needed 59 | 60 | Returns 61 | ------- 62 | string 63 | Name changed to not avoid keywords 64 | 65 | ''' 66 | if name in DEKEYWORDS: 67 | return name[:-1] 68 | return name 69 | -------------------------------------------------------------------------------- /swat/utils/testingmocks.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | Mocks for testing 21 | 22 | ''' 23 | 24 | from unittest import mock 25 | 26 | import swat 27 | 28 | SESSION_ABORTED_MESSAGE = "The Session is no longer active due to an unhandled exception." 29 | 30 | 31 | def mock_getone_session_aborted(connection, datamsghandler=None): 32 | ''' 33 | Mock getting a single "session aborted" response from a connection 34 | 35 | Parameters 36 | ---------- 37 | connection : :class:`CAS` object 38 | The connection/CASAction to get the mock response from. 39 | datamsghandler : :class:`CASDataMsgHandler` object, optional 40 | The object to use for data messages from the server. This is not used in the mock. 41 | 42 | See Also 43 | -------- 44 | :meth:`swat.cas.connection.getone` 45 | 46 | Returns 47 | ------- 48 | :class:`CASResponse` object 49 | 50 | ''' 51 | 52 | # Mock the CAS Response object 53 | with mock.patch('swat.cas.response.CASResponse', autospec=True): 54 | response = swat.cas.response.CASResponse(None) 55 | response.disposition.status = SESSION_ABORTED_MESSAGE 56 | response.disposition.status_code = swat.cas.connection.SESSION_ABORTED_CODE 57 | return response, connection 58 | -------------------------------------------------------------------------------- /cicd/get-version.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Return the version of the package 5 | 6 | This command looks for the `__version__` attribute in the `swat/__init__.py` 7 | file to determine the package version. 8 | 9 | ''' 10 | 11 | from __future__ import print_function, division, absolute_import, unicode_literals 12 | 13 | import argparse 14 | import glob 15 | import os 16 | import re 17 | import sys 18 | 19 | 20 | def print_err(*args, **kwargs): 21 | ''' Print a message to stderr ''' 22 | sys.stderr.write(*args, **kwargs) 23 | sys.stderr.write('\n') 24 | 25 | 26 | def main(args): 27 | ''' Main routine ''' 28 | 29 | version = None 30 | 31 | try: 32 | init = glob.glob(os.path.join(args.root, 'swat', '__init__.py'))[0] 33 | except IndexError: 34 | sys.stderr.write('ERROR: Could not locate swat/__init__.py file\n') 35 | return 1 36 | 37 | with open(init, 'r') as init_in: 38 | for line in init_in: 39 | m = re.search(r'''^__version__\s*=\s*['"]([^'"]+)['"]''', line) 40 | if m: 41 | version = m.group(1) 42 | if version.endswith('-dev'): 43 | version = version.replace('-dev', '.dev0') 44 | 45 | if version: 46 | if args.as_expr: 47 | print('=={}'.format(version)) 48 | else: 49 | print(version) 50 | return 51 | 52 | print_err('ERROR: Could not find __init__.py file.') 53 | 54 | return 1 55 | 56 | 57 | if __name__ == '__main__': 58 | parser = argparse.ArgumentParser(description=__doc__.strip()) 59 | 60 | parser.add_argument('root', type=str, metavar='', nargs='?', 61 | default='.', help='root directory of Python package') 62 | 63 | parser.add_argument('-e', '--as-expr', action='store_true', 64 | help='format the version as a dependency expression') 65 | 66 | args = parser.parse_args() 67 | 68 | sys.exit(main(args) or 0) 69 | -------------------------------------------------------------------------------- /swat/exceptions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | SWAT library exceptions 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | 27 | class SWATError(Exception): 28 | ''' 29 | Base class for all SWAT exceptions 30 | 31 | ''' 32 | pass 33 | 34 | 35 | class SWATOptionError(SWATError): 36 | ''' 37 | SWAT configuration option error 38 | 39 | ''' 40 | pass 41 | 42 | 43 | class SWATCASActionError(SWATError): 44 | ''' 45 | CAS action error exception 46 | 47 | Parameters 48 | ---------- 49 | message : string 50 | The error message 51 | response : CASResponse 52 | The response object that contains the error 53 | connection : CAS 54 | The connection object 55 | results : CASResults or any 56 | The compiled results so far 57 | 58 | ''' 59 | def __init__(self, message, response, connection, results=None, events=None): 60 | super(SWATCASActionError, self).__init__(message) 61 | self.message = message 62 | self.response = response 63 | self.connection = connection 64 | self.results = results 65 | self.events = events 66 | 67 | 68 | class SWATCASActionRetry(SWATError): 69 | ''' 70 | CAS action must be resubmitted 71 | 72 | ''' 73 | -------------------------------------------------------------------------------- /doc/source/sorting.rst: -------------------------------------------------------------------------------- 1 | 2 | .. Copyright SAS Institute 3 | 4 | ******* 5 | Sorting 6 | ******* 7 | 8 | Since data in CAS can be spread across many machines and may even be redistributed 9 | depending on events that occur, the data is not stored in an ordered form. 10 | In general, when using statistical actions, this doesn't make much difference 11 | since the CAS actions doing the work will handle the data regardless of the 12 | order that it is in. However, when you are bringing a table of data back to 13 | the client from CAS using the ``fetch`` action, you may want to have it come 14 | back in a sorted form. 15 | 16 | .. ipython:: python 17 | :suppress: 18 | 19 | import os 20 | import swat 21 | host = os.environ['CASHOST'] 22 | port = os.environ['CASPORT'] 23 | username = os.environ.get('CASUSER', None) 24 | password = os.environ.get('CASPASSWORD', None) 25 | 26 | .. ipython:: python 27 | 28 | conn = swat.CAS(host, port, username, password) 29 | 30 | tbl = conn.read_csv('https://raw.githubusercontent.com/' 31 | 'sassoftware/sas-viya-programming/master/data/cars.csv') 32 | tbl.fetch(to=5) 33 | 34 | tbl.fetch(to=5, sortby=['MSRP']) 35 | 36 | Of course, it is possible to set the direction of the sorting as well. 37 | 38 | .. ipython:: python 39 | 40 | tbl.fetch(to=5, sortby=[{'name':'MSRP', 'order':'descending'}]) 41 | 42 | If you are using the :class:`pandas.DataFrame` style API for :class:`CASTable`, 43 | you can also use the :meth:`sort_values` method on :class:`CASTable` objects. 44 | 45 | .. ipython:: python 46 | 47 | sorttbl = tbl.sort_values(['MSRP']) 48 | sorttbl.head() 49 | 50 | sorttbl = tbl.sort_values(['MSRP'], ascending=False) 51 | sorttbl.head() 52 | 53 | As previously mentioned, this doesn't affect anything in the table on the 54 | CAS server itself, it merely stores away the sort keys and applies them 55 | when data is fetched (either through ``fetch`` directly, or any method that 56 | calls ``fetch`` in the background). 57 | -------------------------------------------------------------------------------- /swat/utils/decorators.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | General utility decorators 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | 27 | class cachedproperty(object): 28 | ''' Property whose value is only calculated once and cached ''' 29 | 30 | def __init__(self, func): 31 | self._func = func 32 | self.__doc__ = func.__doc__ 33 | 34 | def __get__(self, obj, type=None): 35 | if obj is None: 36 | return self 37 | try: 38 | return getattr(obj, '@%s' % self._func.__name__) 39 | except AttributeError: 40 | result = self._func(obj) 41 | setattr(obj, '@%s' % self._func.__name__, result) 42 | return result 43 | 44 | 45 | class getattr_safe_property(object): 46 | ''' Property that safely coexists with __getattr__ ''' 47 | 48 | def __init__(self, func): 49 | self._func = func 50 | self.__doc__ = func.__doc__ 51 | 52 | def __get__(self, obj, type=None): 53 | if obj is None: 54 | return self 55 | try: 56 | return self._func(obj) 57 | except AttributeError as exc: 58 | raise RuntimeError(str(exc)) 59 | 60 | def __set__(self, obj, value): 61 | raise RuntimeError("Setting the '%s' attribute is not allowed" % 62 | self._func.__name__) 63 | -------------------------------------------------------------------------------- /cicd/get-host.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Return a random hostname from the specified collection of names 5 | 6 | This utility is used to randomly choose a hostname from a list 7 | or names with a numeric range. It allows you to put bracketed 8 | lists or ranges of values in one or more places in the hostname 9 | to enumerate the possibilities. For example, if you had test 10 | machines named `test01`, `test02`, and `test03`. You could 11 | retrieve a random item from this list with the following call: 12 | 13 | get-host.py 'test[01,02,03]' 14 | 15 | This would return one of the following: 16 | 17 | test01 18 | test02 19 | test03 20 | 21 | You can also specify numeric ranges: 22 | 23 | get-host.py 'test[01-03]' 24 | 25 | Which would return one of the above results as well. 26 | 27 | ''' 28 | 29 | import argparse 30 | import itertools 31 | import os 32 | import random 33 | import re 34 | import sys 35 | 36 | 37 | def main(args): 38 | ''' Main routine ''' 39 | out = [] 40 | for arg in args.host_expr: 41 | parts = [x for x in re.split(r'(?:\[|\])', arg) if x] 42 | 43 | for i, part in enumerate(parts): 44 | if ',' in part: 45 | parts[i] = re.split(r'\s*,\s*', part) 46 | elif re.match(r'^\d+\-\d+$', part): 47 | start, end = part.split('-') 48 | width = len(start) 49 | start = int(start) 50 | end = int(end) 51 | parts[i] = [('%%0%sd' % width) % x for x in range(start, end + 1)] 52 | else: 53 | parts[i] = [part] 54 | 55 | out += list(''.join(x) for x in itertools.product(*parts)) 56 | 57 | print(random.choice(out)) 58 | 59 | 60 | if __name__ == '__main__': 61 | parser = argparse.ArgumentParser(description=__doc__.strip(), 62 | formatter_class=argparse.RawTextHelpFormatter) 63 | 64 | parser.add_argument('host_expr', type=str, metavar='hostname-expr', nargs='+', 65 | help='hostname expression (ex. myhost[01-06].com)') 66 | 67 | args = parser.parse_args() 68 | 69 | main(args) 70 | -------------------------------------------------------------------------------- /doc/source/index.rst: -------------------------------------------------------------------------------- 1 | 2 | .. Copyright SAS Institute 3 | 4 | :tocdepth: 4 5 | 6 | *************************************************** 7 | SAS Scripting Wrapper for Analytics Transfer (SWAT) 8 | *************************************************** 9 | 10 | .. module:: swat 11 | 12 | **Date**: |today| **Version**: |version| 13 | 14 | **Binary Installers:** ``_ 15 | 16 | **Source Repository:** ``_ 17 | 18 | **Issues & Ideas:** ``_ 19 | 20 | **Q&A Support:** ``_ 21 | 22 | **SAS Viya:** ``_ 23 | 24 | 25 | The **SAS SWAT** package is a Python interface to **SAS Cloud Analytic Services (CAS)** 26 | (the centerpiece of the `SAS Viya `__ framework). 27 | With this package, you can load and analyze 28 | data sets of any size on your desktop or in the cloud. Since CAS can be used on a local 29 | desktop or in a hosted cloud environment, you can analyze extremely large data sets using 30 | as much processing power as you need, while still retaining the ease-of-use of Python 31 | on the client side. 32 | 33 | Using SWAT, you can execute workflows of CAS analytic actions, then pull down 34 | the summarized data to further process on the client side in Python, or to merge with data 35 | from other sources using familiar `Pandas `__ data structures. In fact, 36 | the SWAT package mimics much of the API of the Pandas package so that using CAS should 37 | feel familiar to current Pandas users. 38 | 39 | With the best-of-breed SAS analytics in the cloud and the use of Python and 40 | its large collection of open source packages, the SWAT package gives you access 41 | to the best of both worlds. 42 | 43 | .. toctree:: 44 | :maxdepth: 3 45 | 46 | install 47 | whatsnew 48 | getting-started 49 | workflows 50 | binary-vs-rest 51 | encryption 52 | loading-data 53 | table-vs-dataframe 54 | indexing 55 | bygroups 56 | sorting 57 | api 58 | gotchas 59 | troubleshooting 60 | sas-tk 61 | licenses 62 | 63 | 64 | Index 65 | ================== 66 | 67 | * :ref:`genindex` 68 | 69 | -------------------------------------------------------------------------------- /cicd/download-wheels.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Download Wheel files from URL 5 | 6 | Wheel files may be stored in a staging area before being published to the 7 | public PyPI repository. This is done so that new packages can be tested 8 | before being published officially. 9 | 10 | This utility allows you to download a set of wheel files from a repository 11 | (optionally specifying a specific version of the package) and storing 12 | them in a local directory. They can then be uploaded to the final location 13 | using `twine`. 14 | 15 | ''' 16 | 17 | import argparse 18 | import os 19 | import re 20 | import sys 21 | from urllib.parse import urljoin 22 | from urllib.request import urlopen, urlretrieve 23 | 24 | 25 | def print_err(*args, **kwargs): 26 | ''' Print a message to stderr ''' 27 | sys.stderr.write(*args, **kwargs) 28 | sys.stderr.write('\n') 29 | 30 | 31 | def main(args): 32 | ''' Main routine ''' 33 | if not args.version: 34 | args.version = r'\d+\.\d+\.\d+(\.\w+)*' 35 | 36 | os.makedirs(args.dir, exist_ok=True) 37 | 38 | txt = urlopen(args.url).read().decode('utf-8') 39 | whls = re.findall( 40 | r'href=[\'"](.+?/swat-{}(?:-\d+)?-.+?\.whl)'.format(args.version), txt) 41 | for whl in whls: 42 | url = urljoin(args.url, whl) 43 | print(url) 44 | urlretrieve(url, filename=os.path.join(args.dir, whl.split('/')[-1])) 45 | 46 | return 0 47 | 48 | 49 | if __name__ == '__main__': 50 | parser = argparse.ArgumentParser(description=__doc__.strip(), 51 | formatter_class=argparse.RawTextHelpFormatter) 52 | 53 | parser.add_argument('--version', '-v', type=str, metavar='version', 54 | help='version of package to download') 55 | parser.add_argument('--dir', '-d', type=str, metavar='directory', 56 | default='.', 57 | help='directory to download the files to') 58 | parser.add_argument('url', type=str, metavar='url', 59 | help='URL of PyPI package to download') 60 | 61 | args = parser.parse_args() 62 | 63 | try: 64 | sys.exit(main(args)) 65 | except argparse.ArgumentTypeError as exc: 66 | print_err('ERROR: {}'.format(exc)) 67 | sys.exit(1) 68 | except KeyboardInterrupt: 69 | sys.exit(1) 70 | -------------------------------------------------------------------------------- /swat/tests/cas/test_csess.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | # NOTE: This test requires a running CAS server. You must use an ~/.authinfo 20 | # file to specify your username and password. The CAS host and port must 21 | # be specified using the CASHOST and CASPORT environment variables. 22 | # A specific protocol ('cas', 'http', 'https', or 'auto') can be set using 23 | # the CASPROTOCOL environment variable. 24 | 25 | import swat 26 | import swat.utils.testing as tm 27 | import unittest 28 | 29 | USER, PASSWD = tm.get_user_pass() 30 | HOST, PORT, PROTOCOL = tm.get_host_port_proto() 31 | 32 | 33 | class TestCSess(tm.TestCase): 34 | 35 | def setUp(self): 36 | swat.reset_option() 37 | swat.options.cas.print_messages = False 38 | swat.options.interactive_mode = False 39 | 40 | def tearDown(self): 41 | swat.reset_option() 42 | 43 | # Create a session, loop through a bunch of echo actions 44 | # then terminate the session. Do this in a loop a few times. 45 | # Additional session tests are required. 46 | 47 | def conn(self): 48 | for j in range(1, 6): 49 | self.s = swat.CAS(HOST, PORT, USER, PASSWD) 50 | self.assertNotEqual(self.s, None) 51 | 52 | for i in range(1, 11): 53 | r, z = self.s.help(), self.s.help() 54 | if r.debug is not None: 55 | print(r.debug) 56 | 57 | self.assertNotEqual(r, None) 58 | self.assertEqual(r.status, None) 59 | self.assertEqual(r.debug, None) 60 | self.assertNotEqual(z, None) 61 | 62 | self.s.endsession() 63 | del self.s 64 | 65 | def test_csess_help(self): 66 | self.conn() 67 | 68 | 69 | if __name__ == '__main__': 70 | tm.runtests() 71 | -------------------------------------------------------------------------------- /swat/cas/rest/message.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | Class for simulating CAS messages 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | from .response import REST_CASResponse 27 | 28 | 29 | class REST_CASMessage(object): 30 | ''' CASMessage wrapper ''' 31 | 32 | def __init__(self, obj, connection=None): 33 | ''' 34 | Create a CASMessage object 35 | 36 | Parameters 37 | ---------- 38 | obj : any 39 | The object returned by the CAS connection 40 | connection : REST_CASConnection 41 | The connection the object came from 42 | 43 | Returns 44 | ------- 45 | REST_CASMessage 46 | 47 | ''' 48 | self._obj = obj 49 | self._connection = connection 50 | 51 | def getTypeName(self): 52 | ''' Get the object type ''' 53 | return 'message' 54 | 55 | def getSOptions(self): 56 | ''' Get the SOptions value ''' 57 | return '' 58 | 59 | def isNULL(self): 60 | ''' Is this a NULL object? ''' 61 | return False 62 | 63 | def getTag(self): 64 | ''' Get the message tag ''' 65 | return '' 66 | 67 | def getType(self): 68 | ''' Get the message type ''' 69 | return 'response' 70 | 71 | def getFlags(self): 72 | ''' Get the message flags ''' 73 | return [] 74 | 75 | def toResponse(self, connection=None): 76 | ''' Convert the message to a response ''' 77 | return REST_CASResponse(self._obj) 78 | 79 | def toRequest(self): 80 | ''' Convert the message to a request ''' 81 | raise NotImplementedError('Not supported in the REST interface') 82 | 83 | def getLastErrorMessage(self): 84 | ''' Return the last generated error message ''' 85 | return '' 86 | -------------------------------------------------------------------------------- /swat/cas/utils/misc.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | Utilities for CAS modules 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | import os 27 | 28 | 29 | def super_dir(cls, obj): 30 | ''' 31 | Return the super's dir(...) list 32 | 33 | Parameters 34 | ---------- 35 | cls : type 36 | The type of the object for the super(...) call 37 | obj : instance 38 | The instance object for the super(...) call 39 | 40 | Returns 41 | ------- 42 | list-of-strings 43 | 44 | ''' 45 | if obj is None: 46 | return [] 47 | 48 | try: 49 | return sorted(x for x in super(cls, obj).__dir__() if not x.startswith('_')) 50 | 51 | except AttributeError: 52 | 53 | def get_attrs(o): 54 | try: 55 | return list(o.__dict__.keys()) 56 | except AttributeError: 57 | return [] 58 | 59 | out = set(get_attrs(cls)) 60 | for basecls in cls.__bases__: 61 | out.update(get_attrs(basecls)) 62 | out.update(super_dir(basecls, obj)) 63 | out.update(get_attrs(obj)) 64 | 65 | return list(str(x).decode('utf8') for x in sorted(out) if not x.startswith('_')) 66 | 67 | 68 | def any_file_exists(files): 69 | ''' 70 | Determine if any specified files actually exist 71 | 72 | Parameters 73 | ---------- 74 | files : string or list-of-strings or None 75 | If string, the value is the filename. If list, a boolean is returned 76 | indicating if any of the files exist. 77 | 78 | ''' 79 | if isinstance(files, (list, tuple, set)): 80 | for item in files: 81 | if os.path.isfile(os.path.expanduser(item)): 82 | return True 83 | 84 | elif os.path.isfile(os.path.expanduser(files)): 85 | return True 86 | 87 | return False 88 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' Install the SAS Scripting Wrapper for Analytics Transfer (SWAT) module ''' 20 | 21 | import glob 22 | import io 23 | import os 24 | from setuptools import setup, find_packages 25 | 26 | 27 | def get_file(fname): 28 | with io.open(os.path.join(os.path.dirname(os.path.abspath(__file__)), fname), 29 | encoding='utf8') as infile: 30 | return infile.read() 31 | 32 | 33 | setup( 34 | zip_safe=False, 35 | name='swat', 36 | version='1.17.1-dev', 37 | description='SAS Scripting Wrapper for Analytics Transfer (SWAT)', 38 | long_description=get_file('README.md'), 39 | long_description_content_type='text/markdown', 40 | author='SAS', 41 | author_email='Kevin.Smith@sas.com', 42 | url='http://github.com/sassoftware/python-swat/', 43 | license='Apache v2.0 (SWAT) + SAS Additional Functionality (SAS TK)', 44 | packages=find_packages(), 45 | package_data={ 46 | 'swat': ['lib/*/*.*', 'tests/datasources/*.*', 'readme.md'], 47 | }, 48 | install_requires=[ 49 | 'pandas >= 0.16.0', 50 | 'pytz', 51 | 'six >= 1.9.0', 52 | 'requests', 53 | 'urllib3', 54 | ], 55 | platforms='any', 56 | classifiers=[ 57 | 'Development Status :: 5 - Production/Stable', 58 | 'Environment :: Console', 59 | 'Intended Audience :: Science/Research', 60 | 'Programming Language :: Python', 61 | 'Programming Language :: Python :: 3', 62 | 'Programming Language :: Python :: 3.7', 63 | 'Programming Language :: Python :: 3.8', 64 | 'Programming Language :: Python :: 3.9', 65 | 'Programming Language :: Python :: 3.10', 66 | 'Programming Language :: Python :: 3.11', 67 | 'Programming Language :: Python :: 3.12', 68 | 'Programming Language :: Python :: 3.13', 69 | 'Topic :: Scientific/Engineering', 70 | ], 71 | ) 72 | -------------------------------------------------------------------------------- /swat/utils/datetime.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | Datetime utilities 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | import re 27 | import six 28 | from ..config import get_option 29 | 30 | 31 | def is_datetime_format(fmt): 32 | ''' 33 | Is the given format name a datetime format? 34 | 35 | Parameters 36 | ---------- 37 | fmt : string 38 | Name of a SAS format 39 | 40 | Returns 41 | ------- 42 | bool 43 | 44 | ''' 45 | if not fmt: 46 | return False 47 | dt_formats = get_option('cas.dataset.datetime_formats') 48 | if isinstance(dt_formats, six.string_types): 49 | dt_formats = [dt_formats] 50 | datetime_regex = re.compile(r'^(%s)(\d*\.\d*)?$' % '|'.join(dt_formats), flags=re.I) 51 | return bool(datetime_regex.match(fmt)) 52 | 53 | 54 | def is_date_format(fmt): 55 | ''' 56 | Is the given format name a date format? 57 | 58 | Parameters 59 | ---------- 60 | fmt : string 61 | Name of a SAS format 62 | 63 | Returns 64 | ------- 65 | bool 66 | 67 | ''' 68 | if not fmt: 69 | return False 70 | d_formats = get_option('cas.dataset.date_formats') 71 | if isinstance(d_formats, six.string_types): 72 | d_formats = [d_formats] 73 | date_regex = re.compile(r'^(%s)(\d*\.\d*)?$' % '|'.join(d_formats), flags=re.I) 74 | return bool(date_regex.match(fmt)) 75 | 76 | 77 | def is_time_format(fmt): 78 | ''' 79 | Is the given format name a time format? 80 | 81 | Parameters 82 | ---------- 83 | fmt : string 84 | Name of a SAS format 85 | 86 | Returns 87 | ------- 88 | bool 89 | 90 | ''' 91 | if not fmt: 92 | return False 93 | t_formats = get_option('cas.dataset.time_formats') 94 | if isinstance(t_formats, six.string_types): 95 | t_formats = [t_formats] 96 | time_regex = re.compile(r'^(%s)(\d*\.\d*)?$' % '|'.join(t_formats), flags=re.I) 97 | return bool(time_regex.match(fmt)) 98 | -------------------------------------------------------------------------------- /swat/render/html.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | Functions for rendering output in a ODS-like manner. 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | from IPython.display import display_html, HTML 27 | from pprint import pformat 28 | 29 | STYLESHEET = ''' 30 | 62 | ''' 63 | 64 | 65 | def render_html(results): 66 | ''' 67 | Render an ODS-like HTML report 68 | 69 | Parameters 70 | ---------- 71 | results : CASResults object 72 | 73 | Returns 74 | ------- 75 | None 76 | 77 | ''' 78 | if hasattr(results, '_render_html_'): 79 | out = results._render_html_() 80 | if out is not None: 81 | return display_html(HTML(STYLESHEET + out)) 82 | 83 | if hasattr(results, '_repr_html_'): 84 | out = results._repr_html_() 85 | if out is not None: 86 | return display_html(HTML(out)) 87 | 88 | return display_html(HTML('
%s
' % pformat(results))) 89 | -------------------------------------------------------------------------------- /ContributorAgreement.txt: -------------------------------------------------------------------------------- 1 | Contributor Agreement 2 | 3 | Version 1.1 4 | 5 | Contributions to this software are accepted only when they are 6 | properly accompanied by a Contributor Agreement. The Contributor 7 | Agreement for this software is the Developer's Certificate of Origin 8 | 1.1 (DCO) as provided with and required for accepting contributions 9 | to the Linux kernel. 10 | 11 | In each contribution proposed to be included in this software, the 12 | developer must include a "sign-off" that denotes consent to the 13 | terms of the Developer's Certificate of Origin. The sign-off is 14 | a line of text in the description that accompanies the change, 15 | certifying that you have the right to provide the contribution 16 | to be included. For changes provided in source code control (for 17 | example, via a Git pull request) the sign-off must be included in 18 | the commit message in source code control. For changes provided 19 | in email or issue tracking, the sign-off must be included in the 20 | email or the issue, and the sign-off will be incorporated into the 21 | permanent commit message if the contribution is accepted into the 22 | official source code. 23 | 24 | If you can certify the below: 25 | 26 | Developer's Certificate of Origin 1.1 27 | 28 | By making a contribution to this project, I certify that: 29 | 30 | (a) The contribution was created in whole or in part by me and I 31 | have the right to submit it under the open source license 32 | indicated in the file; or 33 | 34 | (b) The contribution is based upon previous work that, to the best 35 | of my knowledge, is covered under an appropriate open source 36 | license and I have the right under that license to submit that 37 | work with modifications, whether created in whole or in part 38 | by me, under the same open source license (unless I am 39 | permitted to submit under a different license), as indicated 40 | in the file; or 41 | 42 | (c) The contribution was provided directly to me by some other 43 | person who certified (a), (b) or (c) and I have not modified 44 | it. 45 | 46 | (d) I understand and agree that this project and the contribution 47 | are public and that a record of the contribution (including all 48 | personal information I submit with it, including my sign-off) is 49 | maintained indefinitely and may be redistributed consistent with 50 | this project or the open source license(s) involved. 51 | 52 | then you just add a line saying 53 | 54 | Signed-off-by: Random J Developer 55 | 56 | using your real name (sorry, no pseudonyms or anonymous contributions.) 57 | -------------------------------------------------------------------------------- /swat/notebook/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | Utilities for Notebook integration 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | import json 27 | from .. import config 28 | 29 | # _CSS = config.get_suboptions('display.notebook.css') 30 | # _JS = config.get_suboptions('display.notebook.js') 31 | _CSS = _JS = {} 32 | 33 | 34 | # Javascript used to include CSS and Javascript for IPython notebook 35 | def bootstrap(code): 36 | ''' 37 | Return a string that bootstraps the Javascript requirements in IPython notebook 38 | 39 | Parameters 40 | ---------- 41 | code : string 42 | The Javascript code to append 43 | 44 | Returns 45 | ------- 46 | string 47 | The bootstrap code with the given code appended 48 | 49 | ''' 50 | return (r''' 51 | (function ($) { 52 | var custom = $('link[rel="stylesheet"][href^="/static/custom/custom.css"]'); 53 | var head = $('head'); 54 | $.each(%s, function (index, value) { 55 | if ( $('link[rel="stylesheet"][href="' + value + '"]').length == 0 ) { 56 | var e = $('', {type:'text/css', href:value, rel:'stylesheet'}); 57 | if ( custom.length > 0 ) { 58 | custom.before(e); 59 | } else { 60 | head.prepend(e); 61 | } 62 | } 63 | }); 64 | })($); 65 | 66 | require(%s, function () { }, function (err) { 67 | var ids = err.requireModules; 68 | if ( ids && ids.length ) { 69 | var configpaths = %s; 70 | for ( var i = 0; i < ids.length; i++ ) { 71 | var id = ids[i]; 72 | var paths = {}; 73 | paths[id] = configpaths[id]; 74 | requirejs.undef(id); 75 | requirejs.config({paths:paths}); 76 | require([id], function () {}); 77 | } 78 | } 79 | }); 80 | ''' % (json.dumps(list([config.get_option('display.notebook.css.' + x) 81 | for x in sorted(_CSS.keys())])), 82 | json.dumps(list(sorted(_JS.keys()))), 83 | json.dumps({k: config.get_option('display.notebook.js.' + k) for k in _JS}))) \ 84 | + code 85 | -------------------------------------------------------------------------------- /conda.recipe/meta.yaml: -------------------------------------------------------------------------------- 1 | {% set name = 'swat' %} 2 | {% set version = '1.7.0' %} 3 | {% set build = '0' %} 4 | 5 | package: 6 | name: {{ name }} 7 | version: {{ version }} 8 | 9 | source: 10 | url: https://github.com/sassoftware/python-swat/releases/download/v{{ version }}/python-swat-{{ version }}-linux64.tar.gz # [linux64] 11 | url: https://github.com/sassoftware/python-swat/releases/download/v{{ version }}/python-swat-{{ version }}-linux-ppc64le.tar.gz # [ppc64le] 12 | url: https://github.com/sassoftware/python-swat/releases/download/v{{ version }}/python-swat-{{ version }}-win64.tar.gz # [win64] 13 | url: https://github.com/sassoftware/python-swat/archive/v{{ version }}.tar.gz # [not linux64 and not win64 and not ppc64le] 14 | 15 | build: 16 | number: {{ build }} 17 | script: python -m pip install --no-deps --ignore-installed . 18 | # binary repackaging -- don't do relocation, these are part of manylinux1 19 | binary_relocation: False 20 | missing_dso_whitelist: 21 | - /lib/libpthread.so.0 22 | - /lib/libm.so.6 23 | - /lib/libc.so.6 24 | - /lib/libdl.so.2 25 | - /lib/libcrypt.so.1 26 | - /lib/librt.so.1 27 | - /lib/libresolv.so.2 28 | - /lib/libgcc_s.so.1 29 | - /lib/libstdc++.so.6 30 | - /lib/libXt.so.6 # begin xorgpkg.so 31 | - /lib/libXmu.so.6 # 32 | - /lib/libSM.so.6 # 33 | - /lib/libXext.so.6 # 34 | - /lib/libjpeg.so.62 # 35 | - /lib/libICE.so.6 # 36 | - /lib/libfontconfig.so.1 # 37 | - /lib/libXft.so.2 # 38 | - /lib/libpng12.so.0 # 39 | - /lib/libX11.so.6 # end xorgpkg.so 40 | - libdfschr-1.4.so 41 | - libdfssys-1.3.so 42 | - libdfsfio-1.5.so 43 | - libdflic-1.4.so 44 | - libjsig.so 45 | - libjvm.so 46 | - /lib/libgssapi_krb5.so.2 47 | - /lib/libkrb5.so.3 48 | - /lib/libuuid.so.1 49 | - /lib64/libnuma.so.1 50 | 51 | requirements: 52 | build: 53 | - python 54 | - pip 55 | - pandas 56 | - pytz 57 | - six 58 | - requests 59 | - libnuma # [linux64] 60 | - libuuid # [linux64] 61 | - krb5 # [linux64] 62 | run: 63 | - python 64 | - pandas 65 | - pytz 66 | - six 67 | - requests 68 | - libnuma # [linux64] 69 | - libuuid # [linux64] 70 | - krb5 # [linux64] 71 | 72 | test: 73 | imports: 74 | - swat 75 | requires: 76 | - nose 77 | commands: 78 | - nosetests -v swat.tests.cas.test_basics:TestBasics.test_summary 79 | 80 | about: 81 | home: https://github.com/sassoftware/python-swat 82 | license: Apache 2.0 + SAS Additional Functionality 83 | license_file: LICENSE.md 84 | summary: SAS Viya Python Client 85 | doc_url: https://sassoftware.github.io/python-swat/index.html 86 | dev_url: https://github.com/sassoftware/python-swat 87 | -------------------------------------------------------------------------------- /swat/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | SAS Scripting Wrapper for Analytics Transfer (SWAT) 21 | =================================================== 22 | 23 | This package allows you to connect to a SAS CAS host and call actions. 24 | The responses and results are returned as Python objects. 25 | 26 | Create a connection 27 | ------------------- 28 | 29 | >>> s = CAS('myhost.com', 12345, 'username', 'password') 30 | 31 | Load a data table 32 | ----------------- 33 | 34 | >>> tbl = s.read_path('datasources/cars_single.sashdat') 35 | 36 | Load an action set 37 | ------------------ 38 | 39 | >>> s.loadactionset(actionset='simple') 40 | 41 | Get help for an action set 42 | -------------------------- 43 | 44 | >>> help(s.simple) # or s.simple? in IPython 45 | 46 | Get help for an action 47 | ---------------------- 48 | 49 | >>> help(s.summary) # or s.summary? in IPython 50 | 51 | Call an action from the library 52 | ------------------------------- 53 | 54 | >>> result = tbl.summary() 55 | >>> print(result) 56 | >>> print(result.Summary) 57 | 58 | ''' 59 | 60 | from __future__ import print_function, division, absolute_import, unicode_literals 61 | 62 | # Make sure we meet the minimum requirements 63 | import sys 64 | 65 | if sys.hexversion < 0x02070000: 66 | raise RuntimeError('Python 2.7 or newer is required to use this package.') 67 | 68 | # C extension 69 | from .clib import InitializeTK, TKVersion # noqa: E402 70 | 71 | # Configuration 72 | from . import config # noqa: E402 73 | from .config import (set_option, get_option, reset_option, describe_option, 74 | options, option_context) # noqa: E402 75 | 76 | # CAS utilities 77 | from .cas import (CAS, vl, nil, getone, getnext, datamsghandlers, blob) # noqa: E402 78 | from .cas import (dir_actions, dir_members) # noqa: E402 79 | from .cas.table import CASTable # noqa: E402 80 | 81 | # Conflicts with .cas.table, so we import it excplicitly here 82 | from .cas.utils import table # noqa: E402 83 | 84 | # DataFrame with SAS metadata 85 | from .dataframe import SASDataFrame, reshape_bygroups # noqa: E402 86 | 87 | # Functions 88 | from .functions import concat, merge # noqa: E402 89 | 90 | # Exceptions 91 | from .exceptions import SWATError, SWATOptionError, SWATCASActionError # noqa: E402 92 | 93 | # SAS Formatter 94 | from .formatter import SASFormatter # noqa: E402 95 | 96 | __version__ = '1.17.1-dev' 97 | __tk_version__ = None 98 | -------------------------------------------------------------------------------- /swat/tests/cas/test_datapreprocess.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | # NOTE: This test requires a running CAS server. You must use an ~/.authinfo 20 | # file to specify your username and password. The CAS host and port must 21 | # be specified using the CASHOST and CASPORT environment variables. 22 | # A specific protocol ('cas', 'http', 'https', or 'auto') can be set using 23 | # the CASPROTOCOL environment variable. 24 | 25 | 26 | import swat 27 | import swat.utils.testing as tm 28 | import unittest 29 | 30 | USER, PASSWD = tm.get_user_pass() 31 | HOST, PORT, PROTOCOL = tm.get_host_port_proto() 32 | 33 | 34 | class TestDataPreprocess(tm.TestCase): 35 | 36 | # Create a class attribute to hold the cas host type 37 | server_type = None 38 | 39 | def setUp(self): 40 | swat.reset_option() 41 | swat.options.cas.print_messages = False 42 | swat.options.interactive_mode = False 43 | 44 | self.s = swat.CAS(HOST, PORT, USER, PASSWD, protocol=PROTOCOL) 45 | 46 | if type(self).server_type is None: 47 | # Set once per class and have every test use it. 48 | # No need to change between tests. 49 | type(self).server_type = tm.get_cas_host_type(self.s) 50 | 51 | self.srcLib = tm.get_casout_lib(self.server_type) 52 | 53 | r = self.s.loadactionset(actionset='table') 54 | self.assertEqual(r, {'actionset': 'table'}) 55 | 56 | r = self.s.loadactionset(actionset='datapreprocess') 57 | self.assertEqual(r, {'actionset': 'datapreprocess'}) 58 | 59 | r = tm.load_data(self.s, 'datasources/cars_single.sashdat', self.server_type) 60 | 61 | self.tablename = r['tableName'] 62 | self.assertNotEqual(self.tablename, None) 63 | 64 | def tearDown(self): 65 | # tear down tests 66 | self.s.droptable(caslib=self.srcLib, table=self.tablename) 67 | self.s.endsession() 68 | del self.s 69 | self.pathname = None 70 | self.hdfs = None 71 | self.tablename = None 72 | swat.reset_option() 73 | 74 | def test_histogram(self): 75 | r = self.s.datapreprocess.histogram(table={'caslib': self.srcLib, 76 | 'name': self.tablename}, 77 | vars={'MPG_City', 'MPG_Highway'}) 78 | self.assertEqual(r.status, None) 79 | # self.assertEqualsBench(r, 'testdatapreprocess_histogram') 80 | 81 | 82 | if __name__ == '__main__': 83 | tm.runtests() 84 | -------------------------------------------------------------------------------- /cicd/get-basename.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Return the basename for the package 5 | 6 | If no release is specified, only the short version of the basename is returned. 7 | 8 | python-swat-{version} 9 | 10 | If a release is specified, the complete basename is returned. If no platform is 11 | specified, the platform the program is running on is used. 12 | 13 | python-swat-{version}+{release}-{platform} 14 | 15 | ''' 16 | 17 | import argparse 18 | import glob 19 | import os 20 | import platform 21 | import re 22 | import sys 23 | 24 | 25 | def get_platform(): 26 | ''' Return the Anaconda platform name for the current platform ''' 27 | plat = platform.system().lower() 28 | if 'darwin' in plat: 29 | return 'osx-64' 30 | if plat.startswith('win'): 31 | return 'win-64' 32 | if 'linux' in plat: 33 | machine = platform.machine().lower() 34 | if 'x86' in machine: 35 | return 'linux-64' 36 | if 'ppc' in machine: 37 | return 'linux-ppc64le' 38 | return 'unknown' 39 | 40 | 41 | def print_err(*args, **kwargs): 42 | ''' Print a message to stderr ''' 43 | sys.stderr.write(*args, **kwargs) 44 | sys.stderr.write('\n') 45 | 46 | 47 | def main(args): 48 | ''' Main routine ''' 49 | 50 | version = None 51 | tk_version = None 52 | 53 | init = glob.glob(os.path.join(args.root, 'swat', '__init__.py'))[0] 54 | with open(init, 'r') as init_in: 55 | for line in init_in: 56 | m = re.search(r'''^__version__\s*=\s*['"]([^'"]+)['"]''', line) 57 | if m: 58 | version = m.group(1) 59 | if version.endswith('-dev'): 60 | version = version.replace('-dev', '.dev0') 61 | 62 | m = re.search(r'''^__tk_version__\s*=\s*['"]([^'"]+)['"]''', line) 63 | if m: 64 | tk_version = m.group(1) 65 | if tk_version == 'none': 66 | tk_version = 'REST-only' 67 | 68 | if version: 69 | if args.full: 70 | print('python-swat-{}+{}-{}'.format(version, tk_version, args.platform)) 71 | else: 72 | print('python-swat-{}'.format(version)) 73 | return 0 74 | 75 | print_err('ERROR: Could not find __init__.py file.') 76 | 77 | return 1 78 | 79 | 80 | if __name__ == '__main__': 81 | parser = argparse.ArgumentParser(description=__doc__.strip(), 82 | formatter_class=argparse.RawTextHelpFormatter) 83 | 84 | parser.add_argument('root', type=str, metavar='', nargs='?', default='.', 85 | help='root directory of Python package') 86 | 87 | parser.add_argument('--platform', '-p', type=str, metavar='', 88 | choices=['linux-64', 'osx-64', 'win-64', 'linux-ppc64le'], 89 | default=get_platform(), 90 | help='platform of the resulting package') 91 | parser.add_argument('--full', '-f', action='store_true', 92 | help='return the full variant of the basename ' 93 | 'including TK version and platform') 94 | 95 | args = parser.parse_args() 96 | 97 | sys.exit(main(args) or 0) 98 | -------------------------------------------------------------------------------- /doc/source/install.rst: -------------------------------------------------------------------------------- 1 | 2 | .. Copyright SAS Institute 3 | 4 | Installation 5 | ============ 6 | 7 | The SWAT package is installed using the ``pip`` command. The requirements 8 | for using the binary protocol of CAS (recommended) are as follows. 9 | 10 | * **64-bit** Python 3.7 - 3.13 on Linux or Windows 11 | 12 | See additional shared library notes below. 13 | 14 | The binary protocol requires pre-compiled components found in the ``pip`` 15 | installer only. These pieces are not available as source code and 16 | are under a separate license (see :ref:`SAS TK `). The binary protocol 17 | offers better performance than REST, especially when transferring larger 18 | amounts of data. It also offers more advanced data loading from the client 19 | and data formatting features. 20 | 21 | To access the CAS REST interface only, you can use the pure Python code which 22 | runs in Python 3.7 - 3.13. You will still need Pandas installed. While not as 23 | fast as the binary protocol, the pure Python interface is more portable. 24 | For more information, see :ref:`Binary vs. REST `. 25 | 26 | Note that this package is merely a client to a CAS server. It has no utility unless 27 | you have a licensed CAS server to connect to. 28 | 29 | If you do not have ``pip`` installed, you can use ``easy_install pip`` to add 30 | it to your current Python installation. 31 | 32 | 33 | Additional Linux Library Dependencies 34 | ------------------------------------- 35 | 36 | Some Linux distributions may not install all of the needed shared libraries 37 | by default. Most notably, the shared library ``libnuma.so.1`` is required to 38 | make binary protocol connections to CAS. If you do not have this library on 39 | your machine you can install the ``numactl`` package for your distribution 40 | to make it available to SWAT. 41 | 42 | Note that if you use an Anaconda distribution of Python, ``libnuma.so.1`` will 43 | be installed as a dependency automatically. 44 | 45 | 46 | Python Dependencies 47 | ------------------- 48 | 49 | The SWAT package uses many features of the Pandas Python package and other 50 | dependencies of Pandas. If you do not already have version 0.16 or greater 51 | of Pandas installed, ``pip`` will install or update it for you when you 52 | install SWAT. 53 | 54 | If you are using ``pip`` version 23.1 or later to install from a tar.gz file, the python 55 | wheel package is required. If you do not have this package installed, you can install 56 | it using ``pip``. 57 | 58 | PyPI 59 | ---- 60 | 61 | The latest release of SWAT can be installed from PyPI using ``pip`` as follows: 62 | 63 | pip install swat 64 | 65 | 66 | Github 67 | ------ 68 | 69 | SWAT can be installed from ``_. 70 | Simply locate the file for your platform and install it using ``pip`` as 71 | follows:: 72 | 73 | pip install https://github.com/sassoftware/python-swat/releases/download/vX.X.X/python-swat-X.X.X-platform.tar.gz 74 | 75 | Where ``X.X.X`` is the release you want to install, and ``platform`` is the 76 | platform you are installing on. You can also use the source code distribution 77 | if you only want to use the CAS REST interface. It does not contain support 78 | for the binary protocol:: 79 | 80 | pip install https://github.com/sassoftware/python-swat/archive/vX.X.X.tar.gz 81 | -------------------------------------------------------------------------------- /doc/source/encryption.rst: -------------------------------------------------------------------------------- 1 | 2 | .. Copyright SAS Institute 3 | 4 | .. _encryption: 5 | 6 | **************** 7 | Encryption (SSL) 8 | **************** 9 | 10 | If your CAS server is configured to use SSL for communication, you must 11 | configure your certificates on the client side. 12 | 13 | .. note:: Beginning in SAS Viya 3.3, encrypted communication is enabled 14 | in the server by default. 15 | 16 | .. note:: The hostname used when connecting to CAS must match the hostname 17 | in the certificate. 18 | 19 | 20 | Linux, Mac, and REST Protocol on Windows 21 | ======================================== 22 | 23 | Set the following environment variable in the environment 24 | where you are running Python:: 25 | 26 | CAS_CLIENT_SSL_CA_LIST='/path/to/certificates.pem' 27 | 28 | The path indicated here is a client-side path, so the certificates are 29 | typically copied to a local directory from the server. 30 | 31 | 32 | Windows Binary CAS Protocol 33 | =========================== 34 | 35 | If you are using the binary protocol on Windows, you must import the 36 | certificate into the Windows Certificate Store. Documentation on this 37 | process is located at: 38 | 39 | https://go.documentation.sas.com/?docsetId=secref&docsetTarget=n12036intelplatform00install.htm&docsetVersion=9.4&locale=en 40 | 41 | 42 | Troubleshooting 43 | =============== 44 | 45 | There are various issues that you may run into when using encryption. The 46 | most common situations are described below. 47 | 48 | 49 | No Certificate is Being Used 50 | ---------------------------- 51 | 52 | If you are trying to connect to a server with SSL enabled, but you haven't 53 | configured the certificate on your client, you will get the following 54 | **(note the status code at the end)**:: 55 | 56 | ERROR: The TCP/IP negClientSSL support routine failed with status 807ff013. 57 | 58 | This error should be alleviated by setting the path to the correct 59 | certificate in the CAS_CLIENT_SSL_CA_LIST environment variable as shown 60 | in the section above. 61 | 62 | 63 | Incorrect Certificate is Being Used 64 | ----------------------------------- 65 | 66 | If you are using the wrong certificate for your server, you will get an 67 | error like the following **(note the status code at the end)**:: 68 | 69 | ERROR: The TCP/IP negClientSSL support routine failed with status 807ff008. 70 | 71 | This error should be alleviated by setting the path to the correct 72 | certificate in the CAS_CLIENT_SSL_CA_LIST environment variable as shown 73 | in the section above. 74 | 75 | 76 | Unable to Locate libssl on the Client Side 77 | ------------------------------------------ 78 | 79 | The following error indicates that the client can not locate the shared library 80 | libssl on your machine **(note the status code at the end)**:: 81 | 82 | ERROR: The TCP/IP negClientSSL support routine failed with status 803fd068 83 | 84 | This error seems to be most common on Ubuntu Linux systems. To alleviate the 85 | situation, you can either point the client directly to your libssl file using 86 | an environment variable as follows (substituting in your libssl version):: 87 | 88 | TKESSL_OPENSSL_LIB=/path/to/libs/libssl.so.1.0.0 89 | 90 | Or, you can use the more general ld library path environment variable:: 91 | 92 | LD_LIBRARY_PATH=/path/to/libs 93 | 94 | 95 | Server-Side Issues 96 | ------------------ 97 | 98 | If you are still getting SSL errors, you should check your server logs and make 99 | sure that it has been correctly configured and can find the SSL libraries as well. 100 | -------------------------------------------------------------------------------- /swat/tests/cas/test_casa.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | # NOTE: This test requires a running CAS server. You must use an ~/.authinfo 20 | # file to specify your username and password. The CAS host and port must 21 | # be specified using the CASHOST and CASPORT environment variables. 22 | # A specific protocol ('cas', 'http', 'https', or 'auto') can be set using 23 | # the CASPROTOCOL environment variable. 24 | 25 | import os 26 | import swat 27 | import swat.utils.testing as tm 28 | import unittest 29 | 30 | USER, PASSWD = tm.get_user_pass() 31 | HOST, PORT, PROTOCOL = tm.get_host_port_proto() 32 | 33 | 34 | class TestCall(tm.TestCase): 35 | 36 | server_type = None 37 | 38 | def setUp(self): 39 | swat.reset_option() 40 | swat.options.cas.print_messages = False 41 | swat.options.interactive_mode = False 42 | 43 | user, passwd = tm.get_user_pass() 44 | 45 | self.s = swat.CAS(HOST, PORT, USER, PASSWD, protocol=PROTOCOL) 46 | 47 | if type(self).server_type is None: 48 | type(self).server_type = tm.get_cas_host_type(self.s) 49 | 50 | self.srcLib = tm.get_casout_lib(self.server_type) 51 | 52 | def tearDown(self): 53 | # tear down tests 54 | self.s.endsession() 55 | del self.s 56 | swat.reset_option() 57 | 58 | def test_dynamic_table_open(self): 59 | r = self.s.loadactionset(actionset='actionTest') 60 | if r.severity != 0: 61 | self.skipTest("actionTest failed to load") 62 | 63 | r = self.s.loadactionset(actionset='sessionProp') 64 | 65 | r = tm.load_data(self.s, 'datasources/cars_single.sashdat', self.server_type) 66 | 67 | self.tablename = r['tableName'] 68 | self.assertNotEqual(self.tablename, None) 69 | 70 | r = self.s.sessionProp.setsessopt(caslib=self.srcLib) 71 | 72 | r = self.s.actionTest.testdynamictable(tableinfo=self.tablename) 73 | self.assertIn("NOTE: Table '" + self.tablename + "':", r.messages) 74 | self.assertIn("NOTE: -->Name: " + self.tablename, r.messages) 75 | self.assertIn("NOTE: -->nRecs: 428", r.messages) 76 | self.assertIn("NOTE: -->nVars: 15", r.messages) 77 | 78 | self.s.droptable(caslib=self.srcLib, table=self.tablename) 79 | 80 | def test_reflect(self): 81 | r = self.s.loadactionset(actionset='actionTest') 82 | if r.severity != 0: 83 | self.skipTest("actionTest failed to load") 84 | 85 | self.assertEqual(r, {'actionset': 'actionTest'}) 86 | r = self.s.builtins.reflect(actionset="actionTest") 87 | self.assertEqual(r[0]['name'], 'actionTest') 88 | self.assertEqual(r[0]['label'], 'Test') 89 | if 'autoRetry' in r[0]['actions'][0]: 90 | del r[0]['actions'][0]['autoRetry'] 91 | self.assertEqual(r[0]['actions'][0], 92 | {'desc': 'Test function that calls other actions', 93 | 'name': 'testCall', 'params': []}) 94 | 95 | self.assertEqual(r.status, None) 96 | self.assertNotEqual(r.performance, None) 97 | 98 | 99 | if __name__ == '__main__': 100 | tm.runtests() 101 | -------------------------------------------------------------------------------- /swat/functions.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | Global functions 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | import pandas as pd 27 | from . import dataframe 28 | from .cas import table 29 | 30 | 31 | def concat(objs, **kwargs): 32 | ''' 33 | Concatenate data in given objects 34 | 35 | All objects in the concatenation must be of the same type. 36 | 37 | This function is simply a thin wrapper around `pandas.concat', 38 | 'CASTable.concat`, and `pandas.DataFrame.concat` depending on which 39 | objects are in the concatenation. See the documentation for each 40 | function / method for which parameters are supported. 41 | 42 | Parameters 43 | ---------- 44 | objs : list, optional 45 | List of CASTable or DataFrame objects 46 | **kwargs : keyword arguments, optional 47 | Optional arguments to concatenation function 48 | 49 | See Also 50 | -------- 51 | :func:`pandas.concat` 52 | :meth:`CASTable.concat` 53 | :meth:`pandas.DataFrame.concat` 54 | 55 | Returns 56 | ------- 57 | :class:`CASTable` 58 | If first input is a CASTable 59 | :class:`SASDataFrame` 60 | If first input is a SASDataFrame 61 | :class:`DataFrame` 62 | If first input is a pandas.DataFrame 63 | :class:`Series` 64 | If first input is a pandas.Series 65 | 66 | ''' 67 | objs = [x for x in objs if x is not None] 68 | if not objs: 69 | raise ValueError('There are no non-None objects in the given sequence') 70 | 71 | if isinstance(objs[0], table.CASTable): 72 | return table.concat(objs, **kwargs) 73 | 74 | if isinstance(objs[0], dataframe.SASDataFrame): 75 | return dataframe.concat(objs, **kwargs) 76 | 77 | return pd.concat(objs, **kwargs) 78 | 79 | 80 | def merge(left, right, **kwargs): 81 | ''' 82 | Merge data in given objects 83 | 84 | All objects in the merge must be of the same type. 85 | 86 | This function is simply a thin wrapper around `pandas.merge' or 87 | 'CASTable.merge` depending on which objects are in the merge. 88 | See the documentation for each function / method for which parameters 89 | are supported. 90 | 91 | Parameters 92 | ---------- 93 | left : CASTable or SASDataFrame or DataFrame, optional 94 | CASTable or (SAS)DataFrame object 95 | right : CASTable or SASDataFrame or DataFrame, optional 96 | CASTable or (SAS)DataFrame object to merge with 97 | **kwargs : keyword arguments, optional 98 | Optional arguments to merge function 99 | 100 | See Also 101 | -------- 102 | :func:`pandas.merge` 103 | :meth:`CASTable.merge` 104 | 105 | Returns 106 | ------- 107 | :class:`CASTable` 108 | If first input is a CASTable 109 | :class:`SASDataFrame` 110 | If first input is a SASDataFrame 111 | :class:`DataFrame` 112 | If first input is a pandas.DataFrame 113 | :class:`Series` 114 | If first input is a pandas.Series 115 | 116 | ''' 117 | if isinstance(left, table.CASTable): 118 | return table.merge(left, right, **kwargs) 119 | return pd.merge(left, right, **kwargs) 120 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # SAS SWAT Developer How-To 2 | 3 | Developing SWAT using the REST interface is just like developing any 4 | other project on GitHub. You clone the project, do your work, 5 | and submit a pull request. However, the binary interface is a bit 6 | different since it requires the bundled SAS TK libraries and Python 7 | C extension modules. 8 | 9 | ## Developing Against the Binary CAS Interface 10 | 11 | In order to run against CAS using the binary interface, you must copy 12 | the C libraries from a platform-specific distribution to your git 13 | clone. These files are located in the swat/lib// directory. 14 | So to develop on Linux, you would clone SWAT from GitHub, download the 15 | Linux-specific tar.gz file, unzip it, and copy the swat/lib/linux/\*.so 16 | files to your clone directory. From that point on, you should be able 17 | to connect to both REST and binary CAS ports from your clone. 18 | 19 | ## Submitting a Pull Request 20 | 21 | Submitting a pull request uses the standard process at GitHub. 22 | Note that in the submitted changes, there must always be a unit test 23 | for the code being contributed. Pull requests that do not have a 24 | unit test will not be accepted. 25 | 26 | You also must include the text from the ContributerAgreement.txt file 27 | along with your sign-off verifying that the change originated from you. 28 | 29 | ## Testing 30 | 31 | For the most part, testing the SAS SWAT package is just like testing 32 | any other Python package. Tests are written using the standard unittest 33 | package. All test cases are subclasses of TestCase. Although we do 34 | define our own TestCase class in swat.utils.testing so that we can add 35 | extended functionality. 36 | 37 | Since CAS is a network resource and requires authentication, there is 38 | some extra setup involved in getting your tests configured to run 39 | against your CAS server. Normally this involves setting the following 40 | environment variables. 41 | 42 | * CASHOST - the hostname or IP address of your CAS server (Default: None) 43 | * CASPORT - the port of your CAS server (Default: None) 44 | * CASPROTOCOL - the protocol being using ('cas', 'http', 'https' or 'auto'; Default: 'cas') 45 | 46 | * CASUSER - the CAS account username (Default: None) 47 | * CASPASSWORD - the CAS account password (Default: None) 48 | 49 | * CASDATALIB - the CASLib where data sources are found (Default: CASTestTmp) 50 | * CASMPPDATALIB - the CASLib on MPP servers where the data sources are found (Default: HPS) 51 | * CASOUTLIB - the CASLib to use for output CAS tables (Default: CASUSER) 52 | * CASMPPOUTLIB - the CASLib to use for output CAS tables on MPP servers (Default: CASUSER) 53 | 54 | Some of these can alternatively be specified using configuration files. 55 | The CASHOST, CASPORT, and CASPROTOCOL variables can be specified in a .casrc 56 | in your home directory (or in any directory from the directory you are 57 | running from all the way up to your home directory). It is actually written 58 | in Lua, but the most basic form is as follows: 59 | 60 | cashost = 'myhost.com' 61 | casport = 5570 62 | casprotocol = 'cas' 63 | 64 | The CASUSER and CASPASSWORD variables are usually extracted from your 65 | `~/.authinfo` file automatically. The only reason you should use environment 66 | variables is if you have a generalized test running account that is 67 | shared across various tools. 68 | 69 | Finally, the CAS*DATALIB and CAS*OUTLIB variables configured where your 70 | data sources and output tables reside. Using the CASDATALIB location 71 | will make your tests run more efficiently since the test cases can load 72 | the data from a server location. If you don't specify a CASDATALIB (or 73 | the specified one doesn't exist), the data files will be uploaded to the 74 | server for each test (which will result in hundreds of uploads). Most 75 | people will likely set them all to CASUSER and create a directory called 76 | `datasources` in their home directory with the contents of the 77 | `swat/tests/datasources/` directory. 78 | 79 | Once you have these setup, you can use tools like nosetest to run the suite: 80 | 81 | nosetests -v swat.tests 82 | 83 | You can also run each test individually using profiling as follows: 84 | 85 | python swat/tests/cas/test_basics.py --profile 86 | -------------------------------------------------------------------------------- /swat/tests/test_formatter.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | # NOTE: This test requires a running CAS server. You must use an ~/.authinfo 20 | # file to specify your username and password. The CAS host and port must 21 | # be specified using the CASHOST and CASPORT environment variables. 22 | # A specific protocol ('cas', 'http', 'https', or 'auto') can be set using 23 | # the CASPROTOCOL environment variable. 24 | 25 | import numpy as np 26 | import re 27 | import swat 28 | import swat.utils.testing as tm 29 | import unittest 30 | from swat.utils.compat import int32, int64 31 | from swat.formatter import SASFormatter 32 | 33 | USER, PASSWD = tm.get_user_pass() 34 | HOST, PORT, PROTOCOL = tm.get_host_port_proto() 35 | 36 | 37 | class TestFormatter(tm.TestCase): 38 | 39 | # Create a class attribute to hold the cas host type 40 | server_type = None 41 | 42 | def setUp(self): 43 | swat.reset_option() 44 | swat.options.cas.print_messages = False 45 | swat.options.interactive_mode = False 46 | 47 | self.s = swat.CAS(HOST, PORT, USER, PASSWD, protocol=PROTOCOL) 48 | 49 | if type(self).server_type is None: 50 | # Set once per class and have every test use it. 51 | # No need to change between tests. 52 | type(self).server_type = tm.get_cas_host_type(self.s) 53 | 54 | r = tm.load_data(self.s, 'datasources/cars_single.sashdat', self.server_type) 55 | 56 | self.tablename = r['tableName'] 57 | self.assertNotEqual(self.tablename, None) 58 | self.table = r['casTable'] 59 | 60 | def tearDown(self): 61 | # tear down tests 62 | try: 63 | self.s.endsession() 64 | except swat.SWATError: 65 | pass 66 | del self.s 67 | swat.reset_option() 68 | 69 | def test_format(self): 70 | f = self.s.SASFormatter() 71 | 72 | out = f.format(np.int32(10)) 73 | self.assertEqual(out, '10') 74 | 75 | out = f.format(int32(10)) 76 | self.assertEqual(out, '10') 77 | 78 | out = f.format(np.int64(10)) 79 | self.assertEqual(out, '10') 80 | 81 | out = f.format(int64(10)) 82 | self.assertEqual(out, '10') 83 | 84 | out = f.format(u'hi there') 85 | self.assertEqual(out, u'hi there') 86 | 87 | out = f.format(b'hi there') 88 | self.assertEqual(out, 'hi there') 89 | 90 | out = f.format(None) 91 | self.assertEqual(out, '') 92 | 93 | out = f.format(np.float64(1.234)) 94 | self.assertEqual(out, '1.234') 95 | 96 | out = f.format(float(1.234)) 97 | self.assertEqual(out, '1.234') 98 | 99 | out = f.format(np.nan) 100 | self.assertEqual(out, 'nan') 101 | 102 | with self.assertRaises(TypeError): 103 | f.format({'hi': 'there'}) 104 | 105 | def test_basic(self): 106 | f = SASFormatter() 107 | out = f.format(np.int32(10)) 108 | self.assertEqual(out, '10') 109 | 110 | def test_locale(self): 111 | f = SASFormatter(locale='en_ES') 112 | out = f.format(np.int32(10)) 113 | self.assertEqual(out, '10') 114 | 115 | def test_missing_format(self): 116 | f = self.s.SASFormatter() 117 | 118 | out = f.format(123.45678, sasfmt='foo7.2') 119 | self.assertEqual(out, '123.45678') 120 | 121 | def test_render_html(self): 122 | out = self.table.summary().Summary._render_html_() 123 | self.assertEqual(len(re.findall('', out)), 11) 124 | self.assertTrue(len(re.findall('= 16) 125 | 126 | 127 | if __name__ == '__main__': 128 | tm.runtests() 129 | -------------------------------------------------------------------------------- /doc/source/binary-vs-rest.rst: -------------------------------------------------------------------------------- 1 | 2 | .. Copyright SAS Institute 3 | 4 | .. _binaryvsrest: 5 | .. currentmodule:: swat 6 | 7 | *************** 8 | Binary vs. REST 9 | *************** 10 | 11 | As we have mentioned in other sections of documentation on SWAT, there 12 | are two methods of connecting to CAS: binary and REST. The binary 13 | protocol is implemented in the :ref:`SAS TK ` subsystem which is bundled as part 14 | of the SWAT installation (on platforms where it is supported). This 15 | protocol packs all data as efficient binary transmissions. 16 | 17 | The REST interface, on the other hand, uses standard HTTP or HTTPS 18 | communication. All requests and responses are done using HTTP 19 | mechanisms and JSON data. This interface does not require any extra 20 | Python extension modules or the SAS TK subsystem; it is all done in 21 | pure Python. This does have the advantage of being more open, but 22 | a performance penalty is incurred because of the extra layer of JSON 23 | processing. 24 | 25 | The pros and cons of each interface are discussed more below. 26 | 27 | 28 | Binary (CAS) Protocol 29 | --------------------- 30 | 31 | The diagram below shows the overall process for a CAS action request 32 | from invoking the action to getting the results back. The places where 33 | conversion from one data format to another occurs is highlighted in red. 34 | As you can see, the only conversions that take place are in the client-side 35 | to convert Python objects to and from binary CAS constructs. All of 36 | these conversions are done in a Python extension written in C in order 37 | to make them as fast and efficient as possible. 38 | 39 | .. image:: _static/binary-workflow.png 40 | 41 | The default protocol for connecting to CAS is the binary form, but the 42 | SWAT client does try to auto-detect the type by sending test packets over 43 | of different types to see which one succeeds. You can specify the binary 44 | communication specifically by using ``protocol="cas"`` in the 45 | connection constructor. 46 | 47 | .. ipython:: python 48 | :verbatim: 49 | 50 | conn = swat.CAS(cashost, casport, protocol='cas') 51 | 52 | The REST protocol requires more conversions due to the fact that 53 | communication is primarily done using JSON. Let's look at that in the 54 | next section. 55 | 56 | 57 | REST Protocol 58 | ------------- 59 | 60 | The next diagram shows the process for a CAS action request using the 61 | REST interface. Again, the conversion processes are highlighted in red. 62 | In this case, you'll see that more steps are involved in calling the 63 | action. The Python objects must be converted to and from JSON on the 64 | client, and the server must also parse the JSON before calling the CAS 65 | action. While the JSON parsing one the client is done in a Python 66 | extension method, there is more processing overhead than with the 67 | binary interface. 68 | 69 | .. image:: _static/rest-workflow.png 70 | 71 | To specify the REST protocol explicitly, you can use the ``protocol='http'`` 72 | option on the connection constructor. 73 | 74 | .. ipython:: python 75 | :verbatim: 76 | 77 | conn = swat.CAS(cashost, casport, protocol='http') 78 | 79 | While there is more processing in the REST interface, there are some 80 | advantages that can make it a better choice. We'll cover the pros and 81 | cons of each protocol in the last section. 82 | 83 | 84 | Synopsis 85 | -------- 86 | 87 | Binary (CAS) Protocol 88 | ~~~~~~~~~~~~~~~~~~~~~ 89 | 90 | Pros 91 | ++++ 92 | 93 | * Fast and efficient; Fewer conversions 94 | * More authentication mechanisms supported 95 | * Supports custom data loaders using data message handlers 96 | * Addition of SAS TK subsystem also includes support for SAS data formats 97 | 98 | Cons 99 | ++++ 100 | 101 | * Platform support is more limited because SAS TK subsystem is a requirement 102 | * Download / installation size is larger due to addition of SAS TK subsystem 103 | 104 | 105 | REST Protocol 106 | ~~~~~~~~~~~~~ 107 | 108 | Pros 109 | ++++ 110 | 111 | * Uses standard HTTP / HTTPS communication 112 | * Code is pure Python, so it can be used on any platform that Pandas runs on 113 | * Smaller download / installation size 114 | 115 | Cons 116 | ++++ 117 | 118 | * Conversion of objects to and from JSON is slower than binary 119 | * Less efficient communication 120 | * Data message handlers are not supported 121 | * Extra data formatting features are not available (unless SAS TK is also installed) 122 | 123 | -------------------------------------------------------------------------------- /cicd/upload-assests.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Upload assets to a given Github release 5 | 6 | This utility uploads a set of assets to an existing Github release 7 | which is specified by the tag for that release. 8 | 9 | ''' 10 | 11 | import argparse 12 | import glob 13 | import os 14 | import re 15 | import requests 16 | import shutil 17 | import subprocess 18 | import sys 19 | import tarfile 20 | from urllib.parse import quote 21 | 22 | 23 | if '--help' not in sys.argv and '-h' not in sys.argv: 24 | try: 25 | GITHUB_TOKEN = os.environ['GITHUB_TOKEN'] 26 | except KeyError: 27 | sys.stderr.write('ERROR: This utility requires a Github ' 28 | 'token for accessing the Github release API.\n') 29 | sys.stderr.write(' The variable should be held in an ' 30 | 'environment variable named GITHUB_TOKEN.\n') 31 | sys.exit(1) 32 | 33 | 34 | def print_err(*args, **kwargs): 35 | ''' Print a message to stderr ''' 36 | sys.stderr.write(*args, **kwargs) 37 | sys.stderr.write('\n') 38 | 39 | 40 | def get_repo(): 41 | ''' Retrieve the repo part of the git URL ''' 42 | cmd = ['git', 'remote', 'get-url', 'origin'] 43 | repo = subprocess.check_output(cmd).decode('utf-8').strip() 44 | repo = re.search(r'github.com/(.+?)\.git$', repo).group(1) 45 | return repo 46 | 47 | 48 | def get_release(tag_name): 49 | ''' Retrieve the upload URL for the given tag ''' 50 | res = requests.get( 51 | 'https://api.github.com/repos/{}/releases/tags/{}'.format(get_repo(), tag_name), 52 | headers=dict(Authorization='token {}'.format(GITHUB_TOKEN), 53 | Accept='application/vnd.github.v3+json')) 54 | 55 | if res.status_code < 400: 56 | return res.json() 57 | 58 | raise RuntimeError('Could not locate tag name: {}'.format(tag_name)) 59 | 60 | 61 | def upload_asset(url, filename): 62 | ''' 63 | Upload an asset to a release 64 | 65 | POST :server/repos/:owner/:repo/releases/:release_id/assets?name=:asset_filename 66 | 67 | ''' 68 | upload_url = url + '?name={}'.format(quote(os.path.split(filename)[-1])) 69 | with open(filename, 'rb') as asset_file: 70 | requests.post( 71 | upload_url, 72 | headers={'Authorization': 'token {}'.format(GITHUB_TOKEN), 73 | 'Accept': 'application/vnd.github.v3+json', 74 | 'Content-Type': 'application/octet-stream'}, 75 | data=asset_file 76 | ) 77 | 78 | 79 | def delete_asset(asset_id): 80 | ''' Delete the resource at the given ID ''' 81 | requests.delete( 82 | 'https://api.github.com/repos/{}/releases/assets/{}'.format(get_repo(), asset_id), 83 | headers=dict(Authorization='token {}'.format(GITHUB_TOKEN), 84 | Accept='application/vnd.github.v3+json'), 85 | json=dict(asset_id=asset_id)) 86 | 87 | 88 | def main(args): 89 | ''' Main routine ''' 90 | release = get_release(args.tag) 91 | 92 | upload_url = release['upload_url'].split('{')[0] 93 | assets = {x['name']: x for x in release['assets']} 94 | 95 | for asset in args.assets: 96 | print(asset) 97 | filename = os.path.split(asset)[-1] 98 | if filename in assets: 99 | if args.force: 100 | delete_asset(assets[filename]['id']) 101 | else: 102 | print_err('WARNING: Asset already exists: {}'.format(asset)) 103 | continue 104 | upload_asset(upload_url, asset) 105 | 106 | return 0 107 | 108 | 109 | if __name__ == '__main__': 110 | parser = argparse.ArgumentParser(description=__doc__.strip(), 111 | formatter_class=argparse.RawTextHelpFormatter) 112 | 113 | parser.add_argument('--tag', '-t', type=str, metavar='tag_name', required=True, 114 | help='tag of release to upload the asset to') 115 | parser.add_argument('--force', '-f', action='store_true', 116 | help='force upload even if asset of the same name exists') 117 | parser.add_argument('assets', type=str, metavar='filename', nargs='+', 118 | help='assets to upload') 119 | 120 | args = parser.parse_args() 121 | 122 | try: 123 | sys.exit(main(args)) 124 | except argparse.ArgumentTypeError as exc: 125 | print_err('ERROR: {}'.format(exc)) 126 | sys.exit(1) 127 | except KeyboardInterrupt: 128 | sys.exit(1) 129 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | # 2 | # Tox configuration for testing Anaconda and pip Python SWAT packages. 3 | # 4 | 5 | [flake8] 6 | ignore = F401,W503,E275 7 | max-line-length = 90 8 | max-complexity = 80 9 | inline-quotes = single 10 | multiline-quotes = single 11 | 12 | [tox] 13 | requires = tox-conda 14 | envlist = {py27,py34,py35,py36,py37}-{conda,pip} 15 | toxworkdir = {env:TOX_WORK_DIR:{toxinidir}/.tox} 16 | skipsdist = True 17 | whitelist_externals = 18 | /usr/bin/rm 19 | 20 | [testenv] 21 | # Emulate path settings of 'conda activate' 22 | setenv = 23 | PATH = {envdir}{:}{envdir}/Library/mingw-w64/bin{:}{envdir}/Library/usr/bin{:}{envdir}/Library/bin{:}{envdir}/Scripts{:}{envdir}/bin{:}{env:PATH} 24 | passenv = 25 | CASURL 26 | CAS_URL 27 | CASHOST 28 | CAS_HOST 29 | CASPORT 30 | CAS_PORT 31 | CASPROTOCOL 32 | CAS_PROTOCOL 33 | CASUSER 34 | CAS_USER 35 | CASPASSWORD 36 | CAS_PASSWORD 37 | CASTOKEN 38 | CAS_TOKEN 39 | CONDA_PKGS_DIRS 40 | CONDA_CHANNEL_URL 41 | CONDA_SUBDIR 42 | PYPI_URL 43 | NOSETESTS_ARGS 44 | SWAT_VERSION_EXPR 45 | WINDIR 46 | conda_deps = 47 | beautifulsoup4 48 | conda>=3.8 49 | coverage 50 | html5lib 51 | lxml 52 | matplotlib 53 | nose 54 | numexpr 55 | pillow 56 | pip 57 | pymysql 58 | pytest 59 | pytz 60 | requests 61 | sas7bdat 62 | scipy 63 | six 64 | sqlalchemy 65 | xarray 66 | xlrd 67 | xlsxwriter 68 | 69 | # cd to anything but the default {toxinidir}. 70 | changedir = {envdir} 71 | 72 | # 73 | # Parent environments for anaconda and pip package commands. 74 | # 75 | [testenv:conda] 76 | commands = 77 | # - /usr/bin/rm -rf {env:CONDA_PKGS_DIRS:/tmp}/swat-*-py[0-9][0-9]_* 78 | - {envbindir}/conda uninstall -y -q swat 79 | {envbindir}/conda install -y -q -c {env:CONDA_CHANNEL_URL:sas-institute} -c sas-institute swat{env:SWAT_VERSION_EXPR:} 80 | {envbindir}/conda list --show-channel-urls swat 81 | {envbindir}/nosetests -v {env:NOSETESTS_ARGS:} {posargs:swat.tests.cas.test_basics} 82 | - {envbindir}/conda uninstall -y -q swat 83 | # - /usr/bin/rm -rf {env:CONDA_PKGS_DIRS:/tmp}/swat-*-py[0-9][0-9]_* 84 | 85 | [testenv:pip] 86 | commands = 87 | - {envbindir}/pip uninstall -yq swat 88 | {envbindir}/pip install --no-cache-dir --index-url {env:PYPI_URL:https://pypi.python.org/simple} swat{env:SWAT_VERSION_EXPR:} 89 | {envbindir}/pip show swat 90 | {envbindir}/nosetests -v {env:NOSETESTS_ARGS:} {posargs:swat.tests.cas.test_basics} 91 | - {envbindir}/pip uninstall -yq swat 92 | 93 | # 94 | # Python 2.7 95 | # 96 | [testenv:py27-conda] 97 | commands = {[testenv:conda]commands} 98 | conda_deps = 99 | {[testenv]conda_deps} 100 | pandas==0.22* 101 | numpy<=1.20 # pandas is incompatible with newer versions 102 | 103 | [testenv:py27-pip] 104 | commands = {[testenv:pip]commands} 105 | conda_deps = 106 | {[testenv]conda_deps} 107 | pandas==0.22* 108 | numpy<=1.20 # pandas is incompatible with newer versions 109 | 110 | # 111 | # Python 3.5 112 | # 113 | [testenv:py35-conda] 114 | commands = {[testenv:conda]commands} 115 | conda_deps = 116 | {[testenv]conda_deps} 117 | pandas==0.23* 118 | numpy<=1.20 # pandas is incompatible with newer versions 119 | 120 | [testenv:py35-pip] 121 | commands = {[testenv:pip]commands} 122 | conda_deps = 123 | {[testenv]conda_deps} 124 | pandas==0.23* 125 | numpy<=1.20 # pandas is incompatible with newer versions 126 | 127 | # 128 | # Python 3.6 129 | # 130 | [testenv:py36-conda] 131 | commands = {[testenv:conda]commands} 132 | conda_deps = 133 | {[testenv]conda_deps} 134 | pandas==0.24* 135 | numpy<=1.20 # pandas is incompatible with newer versions 136 | 137 | [testenv:py36-pip] 138 | commands = {[testenv:pip]commands} 139 | conda_deps = 140 | {[testenv]conda_deps} 141 | pandas==0.24* 142 | numpy<=1.20 # pandas is incompatible with newer versions 143 | 144 | # 145 | # Python 3.7 146 | # 147 | [testenv:py37-conda] 148 | commands = {[testenv:conda]commands} 149 | conda_deps = 150 | {[testenv]conda_deps} 151 | pandas==0.25* 152 | numpy<=1.20 # pandas is incompatible with newer versions 153 | 154 | [testenv:py37-pip] 155 | commands = {[testenv:pip]commands} 156 | conda_deps = 157 | {[testenv]conda_deps} 158 | pandas==0.25* 159 | numpy<=1.20 # pandas is incompatible with newer versions 160 | 161 | # 162 | # Python 3.8 163 | # 164 | [testenv:py38-conda] 165 | commands = {[testenv:conda]commands} 166 | conda_deps = 167 | {[testenv]conda_deps} 168 | pandas 169 | 170 | [testenv:py38-pip] 171 | commands = {[testenv:pip]commands} 172 | conda_deps = 173 | {[testenv]conda_deps} 174 | pandas 175 | -------------------------------------------------------------------------------- /doc/source/table-vs-dataframe.rst: -------------------------------------------------------------------------------- 1 | 2 | .. Copyright SAS Institute 3 | 4 | .. currentmodule:: swat 5 | .. _tblvsdf: 6 | 7 | *************************************** 8 | CASTable vs. DataFrame vs. SASDataFrame 9 | *************************************** 10 | 11 | :class:`CASTable` objects and DataFrame object (either :class:`pandas.DataFrame` or 12 | :class:`SASDataFrame`) act very similar in many ways, but they are extremely different 13 | constructs. :class:`CASTable` objects do not contain actual data. They are simply 14 | a client-side view of the data in a CAS table on a CAS server. DataFrames and 15 | SASDataFrames contain data in-memory on the client machine. 16 | 17 | Since these objects work very much the same way, it can be a little confusing when 18 | you start to work with them. The rule to remember is that if the type of the object 19 | contains "DataFrame" (i.e., DataFrame or SASDataFrame), the data is local. If the 20 | type of the object contains "Table" or "Column" (i.e., CASTable or CASColumn), it is 21 | a client-side view of the data in a CAS table on the server. 22 | 23 | Even though they are very different architectures, :class:`CASTable` objects support 24 | much of the :class:`pandas.DataFrame` API. However, since CAS tables can contain 25 | enormous amounts of data that wouldn't fit into the memory of a single machine, there 26 | are some differences in the way the APIs work. The basic rules to remember 27 | about :class:`CASTable` data access are as follows. 28 | 29 | * If a method returns observation-level data, it will be returned as a new 30 | :class:`CASTable` object. 31 | * If a method returns summarized data, it will return data as a :class:`SASDataFrame`. 32 | 33 | In other words, if the method is going to return a new data set with potentially 34 | huge amounts of data, you will get a new :class:`CASTable` object that is a view of that 35 | data. If the method is going to compute a result of some analysis that is a summary 36 | of the data, you will get a local copy of that result in a :class:`SASDataFrame`. 37 | Most actions allow you to specify a ``casout=`` parameter that allows you to send the 38 | summarized data to a table in the server as well. 39 | 40 | The methods and properties of :class:`CASTable` objects that return a new :class:`CASTable` 41 | object are ``loc``, ``iloc``, ``ix``, ``query``, ``sort_values``, and ``__getitem__`` 42 | (i.e., ``tbl[...]``). The remaining methods will return results. 43 | A sampling of a number of methods common to both :class:`CASTable` and 44 | DataFrame are shown below with the result type of that method. 45 | 46 | ============ ========== ========= 47 | Method CASTable DataFrame 48 | ============ ========== ========= 49 | o.head() DataFrame DataFrame 50 | o.describe() DataFrame DataFrame 51 | o['col'] CASColumn Series 52 | o[['col']] CASTable DataFrame 53 | o.summary() CASResults N/A 54 | ============ ========== ========= 55 | 56 | In the table above, the :class:`CASColumn` is a subclass of :class:`CASTable` 57 | that only references a single column of the CAS table. 58 | 59 | The last entry in the table above is a call to a CAS action called ``summary``. 60 | All CAS actions return a :class:`CASResults` object (which is a subclass of 61 | Python's ordered dictionary). DataFrame's can not call CAS actions, although 62 | you can upload a DataFrame to a CAS table using the :meth:`CAS.upload` method. 63 | 64 | It is possible to convert all of the data from a CAS table into a DataFrame by 65 | using the :meth:`CASTable.to_frame` method, however, you do need to be careful. 66 | It will attempt to pull all of the data down regardless of size. 67 | 68 | :class:`CASColumn` objects work much in the same was as :class:`CASTable` objects 69 | except that they operate on a single column of data like a :class:`pandas.Series`. 70 | 71 | ============ ========= ========= 72 | Method CASColumn Series 73 | ============ ========= ========= 74 | c.head() Series Series 75 | c[c.col > 1] CASColumn Series 76 | ============ ========= ========= 77 | 78 | Pandas DataFrame vs. SASDataFrame 79 | ================================= 80 | 81 | :class:`SASDataFrame` is a subclass of :class:`pandas.DataFrame`. Therefore, anything 82 | you can do with a :class:`pandas.DataFrame` will also work with :class:`SASDataFrame`. 83 | The only difference is that :class:`SASDataFrame` objects contain extra metadata 84 | familiar to SAS users. This includes a title, label, name, a dictionary of extended 85 | attributes for information such as By groups and system titles (``attrs``), and 86 | a dictionary of column metadata (``colinfo``). 87 | 88 | Also, since SAS has both formatted and raw values for By groups, :class:`SASDataFrame` 89 | objects also have a method called :meth:`SASDataFrame.reshape_bygroups` to change 90 | the way that By group information in represented in the DataFrame. See the 91 | :ref:`By group documentation ` for more information. 92 | -------------------------------------------------------------------------------- /swat/utils/compat.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | Smooth out some differences between Python 2 and Python 3 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | try: 27 | from collections.abc import MutableMapping 28 | except (ImportError, AttributeError): 29 | from collections import MutableMapping 30 | import sys 31 | import numpy as np 32 | 33 | ENCODING = sys.getdefaultencoding() 34 | MAX_INT32 = 2**32 / 2 - 1 35 | MIN_INT32 = -MAX_INT32 36 | WIDE_CHARS = (sys.maxunicode > 65535) 37 | 38 | if sys.version_info >= (3, 0): 39 | PY3 = True 40 | PY2 = False 41 | items_types = (list, tuple, set) 42 | dict_types = (dict, MutableMapping) 43 | int32_types = (np.int32,) 44 | int64_types = (np.int64, int) 45 | int_types = int32_types + int64_types 46 | float64_types = (np.float64, float) 47 | num_types = int_types + float64_types 48 | binary_types = (bytes,) 49 | bool_types = (bool, np.bool_) 50 | text_types = (str,) 51 | char_types = binary_types + text_types 52 | int32 = int 53 | int64 = int 54 | float64 = float 55 | 56 | else: 57 | PY3 = False 58 | PY2 = True 59 | items_types = (list, tuple, set) 60 | dict_types = (dict, MutableMapping) 61 | int32_types = (np.int32, int) 62 | int64_types = (np.int64, long) # noqa: F821 63 | int_types = int32_types + int64_types 64 | float64_types = (np.float64, float) 65 | num_types = int_types + float64_types 66 | binary_types = (str, bytes) 67 | bool_types = (bool, np.bool_) 68 | text_types = (unicode,) # noqa: F821 69 | char_types = binary_types + text_types 70 | int32 = int 71 | int64 = long # noqa: F821 72 | float64 = float 73 | 74 | 75 | def patch_pandas_sort(): 76 | ''' Add sort_values to older versions of Pandas DataFrames ''' 77 | import pandas as pd 78 | 79 | if not hasattr(pd.DataFrame, 'sort_values'): 80 | def sort_values(self, by, axis=0, ascending=True, inplace=False, 81 | kind='quicksort', na_position='last'): 82 | ''' `sort` wrapper for new-style sorting API ''' 83 | return self.sort(columns=by, axis=axis, ascending=ascending, inplace=inplace, 84 | kind=kind, na_position=na_position) 85 | 86 | pd.DataFrame.sort_values = sort_values 87 | 88 | def sort_values(self, axis=0, ascending=True, inplace=False, 89 | kind='quicksort', na_position='last'): 90 | ''' `sort` wrapper for new-style sorting API ''' 91 | return self.sort(axis=axis, ascending=ascending, inplace=inplace, 92 | kind=kind, na_position=na_position) 93 | 94 | pd.Series.sort_values = sort_values 95 | 96 | 97 | def a2u(arg, encoding=ENCODING): 98 | ''' 99 | Convert any string type to unicode 100 | 101 | Parameters 102 | ---------- 103 | arg : str, unicode, or bytes 104 | The string to convert 105 | encoding : string 106 | The encoding to use for encoding if needed 107 | 108 | Returns 109 | ------- 110 | Unicode object 111 | 112 | ''' 113 | if arg is None: 114 | return arg 115 | if isinstance(arg, text_types): 116 | return arg 117 | return arg.decode(encoding) 118 | 119 | 120 | def a2b(arg, encoding=ENCODING): 121 | ''' 122 | Convert any string type to bytes 123 | 124 | Parameters 125 | ---------- 126 | arg : str, unicode, or bytes 127 | The string to convert 128 | encoding : string 129 | The encoding to use to for decoding if needed 130 | 131 | Returns 132 | ------- 133 | Bytes object 134 | 135 | ''' 136 | if arg is None: 137 | return arg 138 | if isinstance(arg, binary_types): 139 | if encoding.lower().replace('-', '').replace('_', '') == 'utf8': 140 | return arg 141 | arg = arg.decode(ENCODING) 142 | return arg.encode('utf-8') 143 | 144 | 145 | # Use OrderedDict if possible 146 | try: 147 | from collections import OrderedDict 148 | except ImportError: 149 | OrderedDict = dict 150 | 151 | # Convert any string to native string type 152 | if PY3: 153 | a2n = a2u 154 | else: 155 | a2n = a2b 156 | -------------------------------------------------------------------------------- /doc/source/bygroups.rst: -------------------------------------------------------------------------------- 1 | 2 | .. Copyright SAS Institute 3 | 4 | .. currentmodule:: swat.cas.results 5 | .. _bygroups: 6 | 7 | ****************** 8 | Handling By Groups 9 | ****************** 10 | 11 | If By groups are specified when running a CAS action, the result are returned 12 | with the following behaviors. 13 | 14 | 1. A result key named 'ByGroupInfo' is returned with all of the By group 15 | variable values. 16 | 2. Each By group table is returned in a separate result key with a prefix of 17 | 'ByGroup#.'. 18 | 19 | These behaviors can help when you have a large number of By groups and you 20 | want to process them as they arrive at the client rather than trying to hold 21 | the entire set of results in memory. However, when your result sets are 22 | smaller, you may want to combine all of the By group tables into a single 23 | :class:`pandas.DataFrame`. To help in these situations, the :class:`CASResults` 24 | class defines some helper methods for you. 25 | 26 | .. ipython:: python 27 | :suppress: 28 | 29 | import os 30 | import swat 31 | hostname = os.environ['CASHOST'] 32 | port = os.environ['CASPORT'] 33 | userid = os.environ.get('CASUSER', None) 34 | password = os.environ.get('CASPASSWORD', None) 35 | conn = swat.CAS(hostname, port, userid, password) 36 | tbl = conn.read_csv('https://raw.githubusercontent.com/' 37 | 'sassoftware/sas-viya-programming/master/data/cars.csv') 38 | 39 | Here is what it looks like to run a standard ``summary`` action, and a ``summary`` 40 | action with a By group specified. We will use this output to demonstrate the 41 | By group processing methods. 42 | 43 | .. ipython:: python 44 | 45 | tbl = tbl[['MSRP', 'Horsepower']] 46 | 47 | tbl.summary(subset=['Min', 'Max']) 48 | 49 | tbl.groupby(['Origin', 'Cylinders']).summary(subset=['Min', 'Max']) 50 | 51 | 52 | Selecting Tables by Name Across By Groups 53 | ========================================= 54 | 55 | The :meth:`CASResults.get_tables` method will return all tables with a given 56 | name across By groups in a list. If the results do not contain By groups, 57 | the single table with that name will be returned in a list. This makes it 58 | possible to use :meth:`CASResults.get_tables` the same way whether By 59 | group variables are specified or not. 60 | 61 | .. ipython:: python 62 | 63 | tbl.summary(subset=['Min', 'Max']).get_tables('Summary') 64 | 65 | tbl.groupby(['Origin', 'Cylinders']).summary(subset=['Min', 'Max']).get_tables('Summary') 66 | 67 | The reason that the table name is required is that many CAS actions can 68 | have multiple tables with different names in each By group. 69 | 70 | 71 | Concatenating By Group Tables 72 | ============================= 73 | 74 | While you can use the :meth:`CASResults.get_tables` method to retrieve tables 75 | of a specified name then use the :func:`concat` function to concatenate them 76 | together, there is also a :meth:`CASResults.concat_bygroups` method that you can 77 | use. This method will concatenate all tables with the same name across all 78 | By groups and set the concatenated table under the table's key. 79 | 80 | .. ipython:: python 81 | 82 | tbl.groupby(['Origin', 'Cylinders']).summary(subset=['Min', 'Max']).concat_bygroups() 83 | 84 | By default, this method returns a new :class:`CASResults` object with the 85 | concatenated tables. If you want to modify the :class:`CASResults` object 86 | in place, you can add a ``inplace=True`` option. 87 | 88 | 89 | Selecting a Specific By Group 90 | ============================= 91 | 92 | In addition to selecting tables by name, you can also select a specific 93 | By group in the result by specifying the By variable values. This is 94 | done with the :meth:`CASResults.get_group` method. 95 | 96 | .. ipython:: python 97 | 98 | tbl.groupby(['Origin', 'Cylinders']).summary(subset=['Min', 'Max']).get_group(['Asia', 4]) 99 | 100 | The values given for the By group variable values can be either the raw 101 | value or the formatted value. 102 | 103 | You can also specify the grouping variables as keyword arguments. 104 | 105 | .. ipython:: python 106 | 107 | tbl.groupby(['Origin', 'Cylinders']).summary(subset=['Min', 'Max']).get_group(Origin='Asia', Cylinders=4) 108 | 109 | Multiple Sets of By Groups 110 | ========================== 111 | 112 | Some CAS actions like ``simple.mdsummary`` allow you to specify multiple By group sets. In cases 113 | like this, the keys for each By group set are prefixed with "`ByGroupSet#.`". To select a By group 114 | set, you can use the :meth:`CASResults.get_set` method. This takes a numeric index indicating which 115 | By group set to select. The return value is a new :class:`CASResults` object that contains just 116 | the selected By group set. You can then use the methods above to select tables or concatenate 117 | tables together. 118 | 119 | .. ipython:: python 120 | 121 | tbl.mdsummary(sets=[dict(groupby=["Origin"]), 122 | dict(groupby=["Cylinders"])]) 123 | 124 | tbl.mdsummary(sets=[dict(groupby=["Origin"]), 125 | dict(groupby=["Cylinders"])]).get_set(1).get_group('Asia') 126 | 127 | .. ipython:: python 128 | :suppress: 129 | 130 | conn.close() 131 | -------------------------------------------------------------------------------- /doc/post-process-sphinx-html: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import re 5 | import sys 6 | 7 | html_files = [] 8 | objects = {} 9 | 10 | BASE = sys.argv[1] 11 | 12 | CUSTOM_CSS = r''' 13 | code.docutils > .pre { 14 | color: #505050 !important 15 | } 16 | .rst-content dl:not(.docutils) dl dt { 17 | border-width: 0px !important; 18 | background-color: transparent !important; 19 | margin-bottom: 0px !important; 20 | } 21 | .rst-content table.field-list td.field-body { 22 | padding-top: 1px; 23 | } 24 | .rst-content dl:not(.docutils) dl dt strong { 25 | font-size: 115%; 26 | } 27 | .rst-content dl:not(.docutils) dl dt span { 28 | font-style: italic; 29 | font-weight: normal; 30 | } 31 | .rst-content dl:not(.docutils) dl dt span.classifier:before { 32 | content: ' ('; 33 | } 34 | .rst-content dl:not(.docutils) dl dt span.classifier:after { 35 | content: ')'; 36 | } 37 | .rst-content dl:not(.docutils) dl dt span.optional, 38 | .rst-content dl:not(.docutils) dl dt span.opt-sep { 39 | font-weight: normal; 40 | } 41 | .rst-content dl:not(.docutils) dl dt .classifier-delimiter { 42 | display: none; 43 | } 44 | .rst-content dl:not(.docutils) dl dd strong { 45 | font-weight: bold; 46 | font-size: smaller; 47 | } 48 | ''' 49 | 50 | 51 | def class2link(match): 52 | name = match.group(1) 53 | if name in objects: 54 | return '%s' % (objects[name], name) 55 | return match.group(1) 56 | 57 | 58 | # Get list of HTML files and object-to-file mappings 59 | for root, dirs, files in os.walk(BASE): 60 | for file in files: 61 | if file.lower().endswith('.html'): 62 | html_files.append(os.path.join(root, file)) 63 | if root.endswith('/generated'): 64 | obj = '.'.join(file.split('.')[:-1]) 65 | while obj: 66 | objects[obj] = file 67 | if '.' in obj: 68 | obj = obj.split('.', 1)[-1] 69 | else: 70 | break 71 | 72 | # Process HTML files 73 | for file in html_files: 74 | with open(file, 'r') as html_file: 75 | txt = html_file.read() 76 | 77 | # Merge consecutive tables 78 | txt = re.sub(r'\s*]*>\s*\s*]*>\s*' + 79 | r']*>\s*', 80 | r'', txt) 81 | 82 | # txt = re.sub(r']*>Attributes

\s*]*>\s*(?:\s*' + 83 | # r']*>\s*|\s*' + 84 | # r']*>.*?\s*)+\s*\s*(\s*\s*)', 85 | # r'\1', txt) 86 | 87 | # Fix property rendering to look like normal attribute 88 | txt = re.sub(r']+>\s*\s*(.+?)\s*\s*', 89 | r'\1', txt) 90 | 91 | # Move ", optional" to new construct 92 | txt = re.sub(r',\s*optional\s*', 93 | r', ' + 94 | r'optional', 95 | txt) 96 | 97 | # Convert :class:`Name` constructs to links 98 | txt = re.sub(r':class:`([^`]+)`', class2link, txt) 99 | 100 | # Convert ``self`` to text 101 | txt = re.sub(r'``?self`?`', r'self', txt) 102 | txt = re.sub(r'``?None`?`', r'self', txt) 103 | 104 | # Convert to more commonly used names 105 | txt = re.sub(r'DatetimeColumnMethods', r'CASColumn.dt', txt) 106 | txt = re.sub(r'CharacterColumnMethods', r'CASColumn.str', txt) 107 | txt = re.sub(r'SASColumnMethods', r'CASColumn.sas', txt) 108 | txt = re.sub(r'CASTablePlotter', r'CASTable.plot', txt) 109 | txt = re.sub(r'swat\.cas\.connection\.CAS', r'swat.CAS', txt) 110 | txt = re.sub(r'swat\.dataframe\.SASDataFrame', r'swat.SASDataFrame', txt) 111 | txt = re.sub(r'swat\.formatter\.SASFormatter', r'swat.SASFormatter', txt) 112 | txt = re.sub(r'CASColumn\.dt(\.\w+\.html)', r'DatetimeColumnMethods\1', txt) 113 | txt = re.sub(r'CASColumn\.str(\.\w+\.html)', r'CharacterColumnMethods\1', txt) 114 | txt = re.sub(r'CASColumn\.sas(\.\w+\.html)', r'SASColumnMethods\1', txt) 115 | txt = re.sub(r'CASTable\.plot(\.\w+\.html)', r'CASTablePlotter\1', txt) 116 | txt = re.sub(r'swat\.CAS((\.\w+)?\.html)', r'swat.cas.connection.CAS\1', txt) 117 | txt = re.sub(r'swat\.SASDataFrame((\.\w+)?\.html)', r'swat.dataframe.SASDataFrame\1', txt) 118 | txt = re.sub(r'swat\.SASFormatter((\.\w+)?\.html)', r'swat.formatter.SASFormatter\1', txt) 119 | txt = re.sub(r'\w+\.unx\.sas\.com', r'mycompany.com', txt) 120 | 121 | # Add line-breaks and formatting for known parameter details 122 | txt = re.sub(r'\bDefault: ', r'
Default: ', txt, flags=re.I) 123 | txt = re.sub(r'\bValid Values: ', r'
Valid Values: ', txt, flags=re.I) 124 | txt = re.sub(r'\bNote: ', r'
Note: ', txt, flags=re.I) 125 | 126 | with open(file, 'w') as html_file: 127 | html_file.write(txt) 128 | 129 | 130 | with open(os.path.join(BASE, '_static', 'custom.css'), 'w') as custom_css: 131 | custom_css.write(CUSTOM_CSS.strip()) 132 | -------------------------------------------------------------------------------- /swat/tests/cas/test_echo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | # NOTE: This test requires a running CAS server. You must use an ~/.authinfo 20 | # file to specify your username and password. The CAS host and port must 21 | # be specified using the CASHOST and CASPORT environment variables. 22 | # A specific protocol ('cas', 'http', 'https', or 'auto') can be set using 23 | # the CASPROTOCOL environment variable. 24 | 25 | import swat 26 | import swat.utils.testing as tm 27 | import unittest 28 | 29 | USER, PASSWD = tm.get_user_pass() 30 | HOST, PORT, PROTOCOL = tm.get_host_port_proto() 31 | 32 | 33 | class TestEcho(tm.TestCase): 34 | 35 | def setUp(self): 36 | swat.reset_option() 37 | swat.options.cas.print_messages = False 38 | swat.options.interactive_mode = False 39 | # swat.options.trace_actions = True 40 | 41 | self.s = swat.CAS(HOST, PORT, USER, PASSWD, protocol=PROTOCOL) 42 | 43 | out = self.s.loadactionset(actionset='actionTest') 44 | if out.severity != 0: 45 | self.skipTest("actionTest failed to load") 46 | 47 | def tearDown(self): 48 | # tear down tests 49 | self.s.endsession() 50 | del self.s 51 | swat.reset_option() 52 | 53 | def test_echo_null(self): 54 | r = self.s.actionTest.testecho() 55 | self.assertEqual(r, {}) 56 | self.assertEqual(r.status, None) 57 | # self.assertEqual(r.debug, None) 58 | 59 | def test_echo_str(self): 60 | r = self.s.actionTest.testecho(x='a') 61 | self.assertEqual(r, {'x': 'a'}) 62 | self.assertEqual(r.status, None) 63 | # self.assertEqual(r.debug, None) 64 | 65 | def test_echo_3(self): 66 | r = self.s.actionTest.testecho(x=3) 67 | self.assertEqual(r, {'x': 3}) 68 | self.assertEqual(r.status, None) 69 | # self.assertEqual(r.debug, None) 70 | 71 | def test_echo_false(self): 72 | r = self.s.actionTest.testecho(x=False) 73 | self.assertEqual(r, {'x': 0}) 74 | self.assertEqual(r.status, None) 75 | # self.assertEqual(r.debug, None) 76 | 77 | def test_echo_list(self): 78 | r = self.s.actionTest.testecho(w='a', x='b', y=3, z=False) 79 | self.assertEqual(r, {'w': 'a', 'x': 'b', 'y': 3, 'z': False}) 80 | self.assertEqual(r.status, None) 81 | # self.assertEqual(r.debug, None) 82 | 83 | def test_echo_emptylist(self): 84 | r = self.s.actionTest.testecho(x=[]) 85 | self.assertEqual(r, {'x': []}) 86 | self.assertEqual(r.status, None) 87 | # self.assertEqual(r.debug, None) 88 | 89 | def test_echo_emptydict(self): 90 | r = self.s.actionTest.testecho(x={}) 91 | self.assertEqual(r, {'x': []}) 92 | self.assertEqual(r.status, None) 93 | # self.assertEqual(r.debug, None) 94 | 95 | def test_echo_emptytuple(self): 96 | r = self.s.actionTest.testecho(emptyTuple=()) 97 | self.assertEqual(r, {'emptyTuple': []}) 98 | self.assertEqual(r.status, None) 99 | # self.assertEqual(r.debug, None) 100 | 101 | def test_echo_singletuple(self): 102 | # A tuple with one item is constructed by following a value with a comma. 103 | # On output, tuples are always enclosed in parentheses. 104 | st = 7, 105 | r = self.s.actionTest.testecho(singleTuple=st) 106 | 107 | # Because of the way that results come back from the server, 108 | # there is no way to construct a list or tuple at the output. 109 | # There is always a possibility of mixed keys and non-keys, 110 | # so Python always has to use dictionaries for output objects. 111 | self.assertEqual(r, {'singleTuple': [7]}) 112 | self.assertEqual(r.status, None) 113 | # self.assertEqual(r.debug, None) 114 | 115 | def test_echo_nest(self): 116 | mytuple = 12345, 54321, 'hello!' 117 | r = self.s.actionTest.testecho(w=3, x=4, y={5}, z=6, a=[7], t=mytuple) 118 | self.assertEqual(r, {'w': 3, 'x': 4, 'y': [5], 'z': 6, 'a': [7], 119 | 't': [12345, 54321, 'hello!']}) 120 | self.assertEqual(r.status, None) 121 | # self.assertEqual(r.debug, None) 122 | 123 | def test_echo_nest_parms(self): 124 | r = self.s.actionTest.testecho(x=3, y={0: 5, 'alpha': 'beta'}, 125 | test=4, orange=True, fred=6) 126 | self.assertEqual(r, {'x': 3, 'y': [5, 'beta'], 'test': 4, 127 | 'orange': True, 'fred': 6}) 128 | self.assertEqual(r.status, None) 129 | # self.assertEqual(r.debug, None) 130 | 131 | 132 | if __name__ == '__main__': 133 | tm.runtests() 134 | -------------------------------------------------------------------------------- /swat/cas/magics.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | Magic commands for IPython Notebook 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | import re 27 | import uuid 28 | from IPython.core.magic import Magics, magics_class, line_cell_magic 29 | from ..exceptions import SWATError 30 | 31 | 32 | @magics_class 33 | class CASMagics(Magics): 34 | ''' 35 | Magic class for surfacing special CAS commands 36 | 37 | ''' 38 | 39 | @line_cell_magic 40 | def casds(self, line, cell=None): 41 | ''' 42 | Call datastep.runcode action with cell content as source 43 | 44 | %%casds [ options ] conn-object 45 | 46 | -q, --quiet : Don't display the result 47 | -o var, --output=var : Store output of action in Python variable `var` 48 | 49 | ''' 50 | shell = self.shell 51 | opts, argsl = self.parse_options(line, 'qo:', 'quiet', 'output=') 52 | args = re.split(r'\s+', argsl, 1) 53 | 54 | if 'q' in opts and 'quiet' not in opts: 55 | opts['quiet'] = opts['q'] 56 | if 'o' in opts and 'output' not in opts: 57 | opts['output'] = opts['o'] 58 | 59 | # Get session variable 60 | try: 61 | session = shell.user_ns[args[0]] 62 | except KeyError: 63 | SWATError('No connection object was supplied') 64 | 65 | out = session.retrieve('builtins.loadactionset', actionset='datastep', 66 | _messagelevel='error', _apptag='UI') 67 | if out.status: 68 | raise SWATError(out.status) 69 | 70 | code = '' 71 | if not cell and len(args) == 2: 72 | code = args[1] 73 | elif cell: 74 | code = cell 75 | 76 | out = session.retrieve('datastep.runcode', code=code, _apptag='UI', 77 | _messagelevel='error') 78 | if out.status: 79 | raise SWATError(out.status) 80 | 81 | if 'quiet' in opts and 'output' not in opts: 82 | return 83 | 84 | if 'output' in opts: 85 | shell.user_ns[opts['output']] = out 86 | 87 | if 'quiet' not in opts: 88 | return out 89 | 90 | @line_cell_magic 91 | def cassql(self, line, cell=None): 92 | ''' 93 | Call fedsql.execdirect action with cell content as source 94 | 95 | %%cassql [ options ] conn-object 96 | 97 | -q, --quiet : Don't display the result 98 | -o var, --output=var : Store output of action in Python variable `var` 99 | -k, --keep : Keep the temporary result table on the server? 100 | 101 | ''' 102 | shell = self.shell 103 | opts, argsl = self.parse_options(line, 'qo:k', 'quiet', 'output=', 'keep') 104 | args = re.split(r'\s+', argsl, 1) 105 | 106 | if 'q' in opts and 'quiet' not in opts: 107 | opts['quiet'] = opts['q'] 108 | if 'o' in opts and 'output' not in opts: 109 | opts['output'] = opts['o'] 110 | if 'k' in opts and 'keep' not in opts: 111 | opts['keep'] = opts['k'] 112 | 113 | # Get session variable 114 | try: 115 | session = shell.user_ns[args[0]] 116 | except KeyError: 117 | SWATError('No connection object was supplied') 118 | 119 | out = session.retrieve('builtins.loadactionset', actionset='fedsql', 120 | _messagelevel='error', _apptag='UI') 121 | if out.status: 122 | raise SWATError(out.status) 123 | 124 | code = '' 125 | if not cell and len(args) == 2: 126 | code = args[1] 127 | elif cell: 128 | code = cell 129 | code = code.strip() 130 | 131 | outtable = '_PY_T_' + str(uuid.uuid4()).replace('-', '_') 132 | out = session.retrieve('fedsql.execdirect', 133 | query=code, casout=outtable, 134 | _apptag='UI', _messagelevel='error') 135 | if out.status: 136 | raise SWATError(out.status) 137 | 138 | if 'quiet' in opts and 'output' not in opts: 139 | return 140 | 141 | if 'output' in opts: 142 | out = session.fetch(table=outtable)['Fetch'] 143 | out.label = None 144 | shell.user_ns[opts['output']] = out 145 | if 'keep' not in opts: 146 | session.retrieve('table.droptable', table=outtable, 147 | _apptag='UI', _messagelevel='error') 148 | 149 | if 'quiet' not in opts: 150 | return out 151 | 152 | 153 | def load_ipython_extension(ipython): 154 | ''' Load extension in IPython ''' 155 | ipython.register_magics(CASMagics) 156 | -------------------------------------------------------------------------------- /doc/source/licenses.rst: -------------------------------------------------------------------------------- 1 | 2 | .. Copyright SAS Institute 3 | 4 | .. _licenses: 5 | 6 | ******** 7 | Licenses 8 | ******** 9 | 10 | The SWAT client includes two pieces each of which uses a separate 11 | license. The Python code is open-sourced and uses the standard 12 | `Apache 2.0 license `_. 13 | This code can be used to access CAS using the REST interface only. 14 | 15 | To use the binary CAS interface, you also need the SAS TK 16 | runtime libraries and the Python extensions compiled against 17 | them. These come bundled in the platform-specific installers for 18 | SWAT and are released under SAS' additional functionality license 19 | (shown below). 20 | 21 | SAS License Agreement for Corrective Code or Additional Functionality 22 | ===================================================================== 23 | 24 | PLEASE CAREFULLY READ THE TERMS AND CONDITIONS OF THIS LICENSE AGREEMENT 25 | ("AGREEMENT"). BY ACCEPTING THIS AGREEMENT AND/OR USING THE CODE, AS 26 | DEFINED BELOW, YOU, ON BEHALF OF CUSTOMER, AS DEFINED BELOW, ARE AGREEING 27 | TO THESE TERMS AND SAS, AS DEFINED BELOW, WILL AUTHORIZE YOU TO DOWNLOAD 28 | THE CODE. YOU REPRESENT AND WARRANT THAT YOU HAVE FULL AUTHORITY TO 29 | BIND CUSTOMER TO THIS AGREEMENT. IF YOU DO NOT AGREE TO ALL OF THE TERMS 30 | OF THIS AGREEMENT, DO NOT ACCEPT THIS AGREEMENT OR ATTEMPT TO DOWNLOAD 31 | THE CODE. 32 | 33 | You are downloading software code ("Code") which will become part of 34 | a software product ("Software") that is currently licensed from SAS 35 | Institute Inc. or one of its subsidiaries ("SAS") under a separate 36 | software license agreement ("Software License Agreement"). The legal 37 | entity that entered into the Software License Agreement with SAS is 38 | defined as "Customer." The Code is designed either to correct an error 39 | in the Software or to add functionality to the Software. The Code is 40 | governed by the Software License Agreement and this Agreement. If you 41 | are not an authorized user under the Software License Agreement, you may 42 | not download the Code. 43 | 44 | In addition to the terms of the Software License Agreement, the 45 | following terms apply to the Code: 46 | 47 | EXPORT/IMPORT RESTRICTIONS. SAS hereby notifies Customer that the Code 48 | is of United States of America ("United States") origin and United 49 | States export laws and regulations apply to the Code. Both SAS and 50 | Customer agree to comply with these and other applicable export and 51 | import laws and regulations. Customer's compliance obligations include 52 | ensuring (a) that there is no access, download, export, re-export, 53 | import, or distribution of the Code or any underlying information, 54 | technology or data except in full compliance with all laws and 55 | regulations of the United States and in full compliance with any other 56 | applicable laws and regulations; and (b) compliance with restrictions of 57 | countries other than the United States related to exports and imports. 58 | United States export classification information for Software can be 59 | found at SAS' Export Compliance website located at 60 | http://support.sas.com/adminservices/export.html. By accepting the 61 | Agreement and using and/or, if authorized, downloading the Code, 62 | Customer agrees to the foregoing and represents and warrants that 63 | (i) neither Customer nor any User, as defined in the Software License 64 | Agreement, is a party to whom the United States prohibits access to the 65 | Code; (ii) neither Customer nor any User is located in, under control 66 | of, or a national or resident of any country to which export of the 67 | Code is restricted by laws of the United States or other applicable 68 | laws and regulations, including E:1 countries (currently Cuba, Iran, 69 | North Korea, Syria, and Sudan); (iii) neither Customer nor any User 70 | will use the Code in activities directly or indirectly related to the 71 | proliferation of weapons of mass destruction; (iv) neither Customer nor 72 | any User will share access to the Code with a party identified in this 73 | paragraph; and (v) neither Customer nor any User shall further export 74 | the Code without a license or other authorization from the United States. 75 | 76 | Source code from which the Code object code is derived ("Source Code") 77 | is not being provided and is a SAS trade secret to which access is not 78 | authorized. Customer may not reverse assemble, reverse engineer, or 79 | decompile the Code or otherwise attempt to recreate the Source Code, 80 | except to the extent applicable laws specifically prohibit such 81 | restriction. 82 | 83 | Upon termination of the license to use the Software, Customer agrees 84 | to delete and destroy the Code and certify to SAS that the Code has 85 | been deleted and destroyed. 86 | 87 | SAS' LICENSORS DISCLAIM ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING 88 | WITHOUT LIMITATION ANY IMPLIED WARRANTIES OF MERCHANTABILITY OR FITNESS 89 | FOR A PARTICULAR PURPOSE, OR ARISING AS A RESULT OF CUSTOM OR USAGE IN 90 | THE TRADE OR BY COURSE OF DEALING. SAS' LICENSORS PROVIDE THEIR 91 | SOFTWARE "AS IS." 92 | 93 | SAS' LICENSORS SHALL NOT BE LIABLE TO YOU OR CUSTOMER FOR ANY GENERAL, 94 | SPECIAL, DIRECT, INDIRECT, CONSEQUENTIAL, INCIDENTAL OR OTHER DAMAGES 95 | ARISING OUT OF OR RELATED TO THE SOFTWARE OR CODE. 96 | 97 | Customer agrees not to release the results of any benchmarking you 98 | perform on the Code or the Software. 99 | 100 | LGL2100/04AUG16 SAS and all other SAS Institute Inc. product or 101 | service names are registered trademarks or trademarks of SAS Institute 102 | Inc. in the USA and other countries. (R) indicates USA registration. 103 | Other brand and product names are trademarks of their respective 104 | companies. 105 | -------------------------------------------------------------------------------- /swat/notebook/zeppelin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | Utilities for Zeppelin Notebook integration 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | import base64 27 | import html 28 | import pandas as pd 29 | import pprint 30 | import six 31 | import sys 32 | from ..utils.compat import a2b 33 | 34 | 35 | def img2tag(img, fmt='png', **kwargs): 36 | ''' 37 | Convert image data into HTML tag with data URL 38 | 39 | Parameters 40 | ---------- 41 | img : bytes 42 | The image data 43 | **kwargs : keyword arguments 44 | CSS attributes as keyword arguments 45 | 46 | Returns 47 | ------- 48 | HTML string 49 | 50 | ''' 51 | img = b'data:image/' + a2b(fmt) + b';base64,' + base64.b64encode(img.strip()) 52 | css = [] 53 | for key, value in kwargs.items(): 54 | css.append('%s:%s' % (key, value)) 55 | css = css and ("style='%s' " % '; '.join(css)) or '' 56 | return "" % (img.decode('ascii'), css) 57 | 58 | 59 | def show(obj, **kwargs): 60 | ''' Display object using the Zeppelin Display System ''' 61 | if hasattr(obj, '_z_show_'): 62 | obj._z_show_(**kwargs) 63 | 64 | elif hasattr(obj, 'head') and callable(obj.head): 65 | show_dataframe(obj, **kwargs) 66 | 67 | elif hasattr(obj, 'savefig') and callable(obj.savefig): 68 | show_matplotlib(obj, **kwargs) 69 | 70 | elif hasattr(obj, '_repr_png_'): 71 | show_image(obj, fmt='png', **kwargs) 72 | 73 | elif hasattr(obj, '_repr_jpeg_'): 74 | show_image(obj, fmt='jpeg', **kwargs) 75 | 76 | elif hasattr(obj, '_repr_svg_'): 77 | show_svg(obj, **kwargs) 78 | 79 | else: 80 | print('%%html
%s
' % html.escape(pprint.pformat(obj))) 81 | 82 | 83 | def show_image(img, fmt='png', width='auto', height='auto'): 84 | ''' Display an Image object ''' 85 | if fmt == 'png': 86 | img = img2tag(img._repr_png_()) 87 | 88 | elif fmt in ['jpeg', 'jpg']: 89 | img = img2tag(img._repr_jpeg_()) 90 | 91 | else: 92 | raise ValueError("Image format must be 'png' or 'jpeg'.") 93 | 94 | out = "%html
{img}
" 95 | 96 | print(out.format(width=width, height=height, img=img)) 97 | 98 | 99 | def show_svg(img, width='auto', height='auto'): 100 | ''' Display an SVG object ''' 101 | img = img._repr_svg_() 102 | 103 | out = "%html
{img}
" 104 | 105 | print(out.format(width=width, height=height, img=img)) 106 | 107 | 108 | def show_matplotlib(plt, fmt='png', width='auto', height='auto'): 109 | ''' Display a Matplotlib plot ''' 110 | if fmt in ['png', 'jpeg', 'jpg']: 111 | io = six.BytesIO() 112 | plt.savefig(io, format=fmt) 113 | img = img2tag(io.getvalue(), width=width, height=height) 114 | io.close() 115 | 116 | elif fmt == 'svg': 117 | io = six.StringIO() 118 | plt.savefig(io, format=fmt) 119 | img = io.getvalue() 120 | io.close() 121 | 122 | else: 123 | raise ValueError("Image format must be 'png', 'jpeg', or 'svg'.") 124 | 125 | out = "%html
{img}
" 126 | 127 | print(out.format(width=width, height=height, img=img)) 128 | 129 | 130 | def show_dataframe(df, show_index=None, max_result=None, **kwargs): 131 | ''' 132 | Display a DataFrame-like object in a Zeppelin notebook 133 | 134 | Parameters 135 | ---------- 136 | show_index : bool, optional 137 | Should the index be displayed? By default, If the index appears to 138 | simply be a row number (name is None, type is int), the index is 139 | not displayed. Otherwise, it is displayed. 140 | max_result : int, optional 141 | The maximum number of rows to display. Defaults to the Pandas option 142 | ``display.max_rows``. 143 | 144 | ''' 145 | title = getattr(df, 'title', getattr(df, 'label', None)) 146 | if title: 147 | sys.stdout.write('%%html
%s
\n\n' % title) 148 | 149 | sys.stdout.write('%table ') 150 | 151 | rows = df.head(n=max_result or pd.get_option('display.max_rows')) 152 | index = rows.index 153 | 154 | if show_index is None: 155 | show_index = True 156 | if index.names == [None] and str(index.dtype).startswith('int'): 157 | show_index = False 158 | 159 | if show_index and index.names: 160 | sys.stdout.write('\t'.join([x or '' for x in index.names])) 161 | sys.stdout.write('\t') 162 | 163 | sys.stdout.write('\t'.join(rows.columns)) 164 | sys.stdout.write('\n') 165 | 166 | for idx, row in zip(index.values, rows.values): 167 | if show_index: 168 | if isinstance(idx, (list, tuple)): 169 | sys.stdout.write('\t'.join(['%s' % item for item in idx])) 170 | else: 171 | sys.stdout.write('%s' % idx) 172 | sys.stdout.write('\t') 173 | sys.stdout.write('\t'.join(['%s' % item for item in row])) 174 | sys.stdout.write('\n') 175 | -------------------------------------------------------------------------------- /swat/utils/authinfo.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | Utilities for reading authinfo/netrc files 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | import os 27 | import re 28 | import sys 29 | from .compat import items_types 30 | 31 | _AUTHINFO_PATHS = [ 32 | '_authinfo.gpg', 33 | '.authinfo.gpg', 34 | '_netrc.gpg', 35 | '.netrc.gpg', 36 | '_authinfo', 37 | '.authinfo', 38 | '_netrc', 39 | '.netrc', 40 | ] 41 | 42 | if 'win' not in sys.platform.lower(): 43 | _AUTHINFO_PATHS = [aipath for aipath in _AUTHINFO_PATHS if not aipath.startswith('_')] 44 | 45 | _ALIASES = { 46 | 'machine': 'host', 47 | 'login': 'user', 48 | 'account': 'user', 49 | 'port': 'protocol', 50 | } 51 | 52 | 53 | def _chunker(seq, size): 54 | ''' Read sequence `seq` in `size` sized chunks ''' 55 | return (seq[pos:pos + size] for pos in range(0, len(seq), size)) 56 | 57 | 58 | def _matches(params, **kwargs): 59 | ''' See if keyword arguments are a subset of `params` ''' 60 | for key, value in kwargs.items(): 61 | if value is None: 62 | continue 63 | if key not in params: 64 | continue 65 | if params.get(key) != value: 66 | return False 67 | return True 68 | 69 | 70 | def parseparams(param): 71 | ''' 72 | Parse the next parameter from the string 73 | 74 | Parameters 75 | ---------- 76 | param : string 77 | The string to parse 78 | 79 | Returns 80 | ------- 81 | dict 82 | Key/value pairs parsed from the string 83 | 84 | ''' 85 | out = {} 86 | 87 | if not param: 88 | return out 89 | 90 | siter = iter(param) 91 | 92 | name = [] 93 | for char in siter: 94 | if not char.strip(): 95 | break 96 | name.append(char) 97 | 98 | value = [] 99 | for char in siter: 100 | if not char.strip(): 101 | break 102 | if char == '"': 103 | for subchar in siter: 104 | if subchar == '\\': 105 | value.append(next(siter)) 106 | elif subchar == '"': 107 | break 108 | else: 109 | value.append(char) 110 | 111 | name = ''.join(name) 112 | value = ''.join(value) 113 | 114 | out[_ALIASES.get(name, name)] = value 115 | out.update(parseparams((''.join(list(siter))).strip())) 116 | return out 117 | 118 | 119 | def query_authinfo(host, user=None, protocol=None, path=None): 120 | ''' 121 | Look for a matching host definition in authinfo/netrc files 122 | 123 | Parameters 124 | ---------- 125 | host : string 126 | The host name or IP address to match 127 | user : string, optional 128 | The username to match 129 | protocol : string or int, optional 130 | The protocol or port to match 131 | path : string or list of strings, optional 132 | The paths to look for instead of the automatically detected paths 133 | 134 | Returns 135 | ------- 136 | dict 137 | Connection information 138 | 139 | ''' 140 | paths = [] 141 | 142 | # Construct list of authinfo/netrc paths 143 | if path is None: 144 | if os.environ.get('AUTHINFO'): 145 | paths = [os.path.expanduser(x) 146 | for x in os.environ.get('AUTHINFO').split(os.path.sep)] 147 | elif os.environ.get('NETRC'): 148 | paths = [os.path.expanduser(x) 149 | for x in os.environ.get('NETRC').split(os.path.sep)] 150 | else: 151 | home = os.path.expanduser('~') 152 | for item in _AUTHINFO_PATHS: 153 | paths.append(os.path.join(home, item)) 154 | 155 | elif not isinstance(path, items_types): 156 | paths = [os.path.expanduser(path)] 157 | 158 | else: 159 | paths = [os.path.expanduser(x) for x in path] 160 | 161 | # Parse each file 162 | for path in paths: 163 | 164 | if not os.path.exists(path): 165 | continue 166 | 167 | # Remove comments and macros 168 | lines = [] 169 | 170 | with open(path) as info: 171 | infoiter = iter(info) 172 | for line in infoiter: 173 | line = line.strip() 174 | 175 | # Bypass comments 176 | if line.startswith('#'): 177 | continue 178 | 179 | # Bypass macro definitions 180 | if line.startswith('macdef'): 181 | for line in infoiter: 182 | if not line.strip(): 183 | break 184 | continue 185 | 186 | lines.append(line) 187 | 188 | line = ' '.join(lines) 189 | 190 | # Parse out definitions and look for matches 191 | defs = [x for x in re.split(r'\b(host|machine|default)\b\s*', line) if x.strip()] 192 | 193 | for name, value in _chunker(defs, 2): 194 | if name in ['host', 'machine']: 195 | hostname, value = re.split(r'\s+', value, 1) 196 | out = parseparams(value) 197 | out['host'] = hostname.lower() 198 | if _matches(out, host=host.lower(), user=user, protocol=protocol): 199 | return out 200 | else: 201 | out = parseparams(value) 202 | if _matches(out, user=user, protocol=protocol): 203 | return out 204 | 205 | return {} 206 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Change Log 3 | 4 | ## 1.17.0 - 2025-09-30 5 | 6 | - Add Python 3.13 support 7 | - Update TK subsystem 8 | - Add Openssl 3 support 9 | 10 | ## 1.16.0 - 2025-08-15 11 | 12 | - Update TK subsystem 13 | - PowerPC is no longer supported 14 | 15 | ## 1.15.0 - 2024-12-05 16 | 17 | - Add Python 3.12 support 18 | - Update TK subsystem 19 | - Conda packages will no longer be generated 20 | 21 | ## 1.14.0 - 2024-06-07 22 | 23 | - Add support for Proof Key for Code Exchange (PKCE) 24 | - Update TK subsystem 25 | - Wheel and Conda files for Python releases < 3.7 are no longer created 26 | 27 | ## 1.13.3 - 2023-08-31 28 | 29 | - add wheel files for macosx_11_0_arm64 30 | 31 | ## 1.13.2 - 2023-07-31 32 | 33 | - Add CASTable.rename() to rename the columns of a table 34 | - Add biomedical image filetypes to the Image Data Message Handler 35 | 36 | ## 1.13.1 - 2023-07-14 37 | 38 | - Add nunique method for CASTable 39 | - Add drop_duplicates method for CASTable 40 | - Add new testcases to swat/tests/test_dataframe.py and swat/tests/cas/test_table.py 41 | 42 | ## 1.13.0 - 2023-04-20 43 | 44 | - Add Python 3.11 support 45 | - Update TK subsystem 46 | 47 | ## 1.12.2 - 2023-04-14 48 | 49 | - updates to swat/tests/cas/test_imstat.py for benchmark changes 50 | - updates to swat/tests/cas/test_builtins.py for benchmark changes 51 | - cleanup deprecation warning messages 52 | - improve error message when on, onleft, onright merge parameters contain a list rather than a string 53 | 54 | ## 1.12.1 - 2023-01-09 55 | 56 | - Update Authentication documentation 57 | 58 | ## 1.12.0 - 2022-11-11 59 | 60 | - New Image CASDataMsgHandler to allow easy uploading of client-side images to a CAS table 61 | - Update TK subsystem 62 | 63 | ## 1.11.0 - 2022-07-05 64 | 65 | - Update TK subsystem 66 | 67 | ## 1.10.0 - 2022-06-10 68 | 69 | - Add Python 3.10 support 70 | - Update TK subsystem 71 | 72 | ## 1.9.3 - 2021-08-06 73 | 74 | - Fix showlabels issue in Viya deployment before version 3.5 75 | 76 | ## 1.9.2 - 2021-06-18 77 | 78 | - Add authorization code as authentication method 79 | 80 | ## 1.9.1 - 2021-06-11 81 | 82 | - Add Python 3.9 support 83 | 84 | ## 1.9.0 - 2021-05-24 85 | 86 | - Add additional plotting method parameters for correct data fetches 87 | - Add `date_format=` to `CAS.upload` method for formatting CSV exported data to a specific format 88 | - Update TK subsystem 89 | 90 | ## 1.8.1 - 2021-01-21 91 | 92 | - Fix compatibility with pandas 1.2 DataFrame rendering 93 | - Fix TKECERT error by locating libcrypto automatically 94 | 95 | ## 1.8.0 - 2021-01-12 96 | 97 | - Use ssl module's CA list as default 98 | - Improve initial connection performance 99 | - Fix OAuth authentication in REST connections 100 | - Fix log truncation with messages >1000 characters 101 | 102 | ## 1.7.1 - 2020-09-29 103 | 104 | - Add ability to apply SAS formats to columns in a `SASDataFrame` 105 | - Support timezones in data uploaded and downloaded from CAS tables 106 | - Fix issue with TK path on Windows when using flask 107 | 108 | ## 1.7.0 - 2020-08-19 109 | 110 | - Add Python 3.8 support 111 | - Improve connection parameter handling (add CAS_URL= connection variable) 112 | - Improve connection protocol auto-detection 113 | 114 | ## 1.6.1 - 2020-02-10 115 | 116 | - Add pandas v1.0.0 support 117 | 118 | ## 1.6.0 - 2019-11-21 119 | 120 | - Fix letter-casing in `has_action` and `has_actionset` methods 121 | - Remove usage of deprecated `ix` accessor 122 | - Explicitly specify column and line delimiters and locale in uploaded CSV files 123 | - Fix TKPATH issues in Windows and PPC 124 | 125 | ## 1.5.2 - 2019-09-09 126 | 127 | - Fix issue with nulls in REST parameters 128 | - Add fallback default configuration for SSL 129 | - Add `CASTable.get` method 130 | 131 | ## 1.5.1 - 2019-03-01 132 | 133 | - Fix Authinfo matching when using base URLs in REST interface 134 | - Fix compatibility with pandas 0.24 135 | - Fix blob parameters in REST interface 136 | - Add `CASTable.last_modified_date`, `CASTable.last_accessed_date`, and `CASTable.created_date` properties 137 | - Add reverse operator methods to `CASColumn` 138 | 139 | ## 1.5.0 - 2018-09-18 140 | 141 | - Windows support for binary CAS protocol 142 | - Added `with_params` method to `CASTable` to create one-off parameter object 143 | - Set appropriate column data types when uploading a `DataFrame` 144 | 145 | ## 1.4.0 - 2018-07-25 146 | 147 | - Automatic CAS table creation when large number of By groups are generated in some DataFrame methods 148 | - Debugging options for REST interface 149 | - Python 3.7 support 150 | 151 | ## 1.3.1 - 2018-06-04 152 | 153 | - Increase compatibility with older and newer versions of pandas 154 | - Automatically convert columns with SAS date/time formats to Python date/time objects 155 | - Improve `CASTable.merge` algorithm 156 | - Fix autocompletion on `CAS` and `CASTable` objects 157 | 158 | ## 1.3.0 - 2017-12-12 159 | 160 | - Add new summary statistics for new version of CAS 161 | - Improve missing value support in `CASTable` `describe` method 162 | - Add controller failover support 163 | - Improve encrypted communication support 164 | - Add `add`, `any`, `all`, `merge`, and `append` methods to `CASTable` 165 | - Add `merge` and `concat` functions with `CASTable` support 166 | 167 | ## 1.2.1 - 2017-09-13 168 | 169 | - Better support for binary data in table uploads and parameters 170 | - Add integer missing value support 171 | - Allow list parameters to also be sets 172 | - Improve connection protocol detection 173 | - Add `eval` method to `CASTable` 174 | 175 | ## 1.2.0 - 2017-05-02 176 | 177 | - Use `upload` action rather than `addtable` for `read_*` methods. 178 | - Add basic Zeppelin notebook support (`from swat.notebook.zeppelin import show`) 179 | 180 | ## 1.1.0 - 2017-03-21 181 | 182 | - Add support for Python 3.6 (Linux extension) 183 | - Implement `sample` method on `CASTable` 184 | - Add sampling support to plotting methods 185 | - `cas.dataset.max_rows_fetched` increased to 10,000 186 | - Add `terminate` method to `CAS` object to end session and close connection 187 | - Implement `fillna`, `replace`, and `dropna` methods on `CASTable` 188 | - Add `apply_labels` method on `SASDataFrame` to set column labels as column names 189 | 190 | ## 1.0.0 - 2016-09-27 191 | 192 | - Initial Release 193 | -------------------------------------------------------------------------------- /swat/tests/cas/test_response.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | # NOTE: This test requires a running CAS server. You must use an ~/.authinfo 20 | # file to specify your username and password. The CAS host and port must 21 | # be specified using the CASHOST and CASPORT environment variables. 22 | # A specific protocol ('cas', 'http', 'https', or 'auto') can be set using 23 | # the CASPROTOCOL environment variable. 24 | 25 | import copy 26 | import json 27 | import numpy as np 28 | import os 29 | import pandas as pd 30 | import six 31 | import swat 32 | import swat.utils.testing as tm 33 | import sys 34 | import unittest 35 | from bs4 import BeautifulSoup 36 | 37 | USER, PASSWD = tm.get_user_pass() 38 | HOST, PORT, PROTOCOL = tm.get_host_port_proto() 39 | 40 | 41 | class TestCASResponse(tm.TestCase): 42 | 43 | # Create a class attribute to hold the cas host type 44 | server_type = None 45 | 46 | def setUp(self): 47 | swat.reset_option() 48 | swat.options.cas.print_messages = False 49 | swat.options.interactive_mode = False 50 | 51 | self.s = swat.CAS(HOST, PORT, USER, PASSWD, protocol=PROTOCOL) 52 | 53 | if type(self).server_type is None: 54 | # Set once per class and have every test use it. 55 | # No need to change between tests. 56 | type(self).server_type = tm.get_cas_host_type(self.s) 57 | 58 | self.srcLib = tm.get_casout_lib(self.server_type) 59 | 60 | r = tm.load_data(self.s, 'datasources/cars_single.sashdat', self.server_type) 61 | 62 | self.tablename = r['tableName'] 63 | self.assertNotEqual(self.tablename, None) 64 | self.table = r['casTable'] 65 | 66 | def tearDown(self): 67 | # tear down tests 68 | try: 69 | self.s.endsession() 70 | except swat.SWATError: 71 | pass 72 | del self.s 73 | swat.reset_option() 74 | 75 | def test_basic(self): 76 | self.table.loadactionset('simple') 77 | out = self.table.summary() 78 | 79 | self.assertEqual(len(out), 1) 80 | self.assertEqual(set(out.keys()), set(['Summary'])) 81 | 82 | self.table.groupBy = {"Make"} 83 | 84 | self.table.loadactionset('datapreprocess') 85 | out = self.table.histogram() 86 | 87 | self.assertTrue('ByGroupInfo' in out) 88 | self.assertEqual(len(out), 39) 89 | for i in range(1, 39): 90 | self.assertTrue(('ByGroup%d.BinDetails' % i) in out) 91 | 92 | def test_disposition(self): 93 | # The default value of the logflushtime session option causes the response 94 | # messages to be flushed periodically during the action instead of all messages 95 | # flushed when the action completes. Flushing periodically caused this test 96 | # to fail because the test expects all the messages to arrive at once. Set 97 | # logflushtime so all messages will be flushed at once. 98 | self.s.sessionProp.setSessOpt(logflushtime=-1) 99 | 100 | conn = self.table.invoke('loadactionset', actionset='simple') 101 | 102 | messages = [] 103 | disp = None 104 | perf = None 105 | for resp in conn: 106 | messages += resp.messages 107 | disp = resp.disposition 108 | perf = resp.performance 109 | 110 | self.assertIn("NOTE: Added action set 'simple'.", messages) 111 | if not messages[0].startswith('WARNING: License for feature'): 112 | self.assertEqual(disp.to_dict(), dict(debug=None, reason=None, 113 | severity=0, status=None, 114 | status_code=0)) 115 | self.assertEqual(set(perf.to_dict().keys()), 116 | set(['cpu_system_time', 'cpu_user_time', 'elapsed_time', 117 | 'memory', 'memory_os', 'memory_quota', 'system_cores', 118 | 'system_nodes', 'system_total_memory', 119 | 'data_movement_time', 'data_movement_bytes'])) 120 | 121 | def test_str(self): 122 | conn = self.table.invoke('loadactionset', actionset='simple') 123 | 124 | for resp in conn: 125 | out = str(resp) 126 | self.assertTrue(isinstance(out, str)) 127 | self.assertTrue('messages=' in out) 128 | self.assertTrue('performance=' in out) 129 | self.assertTrue('disposition=' in out) 130 | for item in ['cpu_system_time', 'cpu_user_time', 'elapsed_time', 'memory', 131 | 'memory_os', 'memory_quota', 'system_cores', 132 | 'system_nodes', 'system_total_memory']: 133 | self.assertTrue(('%s=' % item) in out) 134 | for item in ['severity', 'reason', 'status', 'debug']: 135 | self.assertTrue(('%s=' % item) in out) 136 | 137 | out = repr(resp) 138 | self.assertTrue(isinstance(out, str)) 139 | self.assertTrue('messages=' in out) 140 | self.assertTrue('performance=' in out) 141 | self.assertTrue('disposition=' in out) 142 | for item in ['cpu_system_time', 'cpu_user_time', 'elapsed_time', 'memory', 143 | 'memory_os', 'memory_quota', 'system_cores', 144 | 'system_nodes', 'system_total_memory']: 145 | self.assertTrue(('%s=' % item) in out) 146 | for item in ['severity', 'reason', 'status', 'debug']: 147 | self.assertTrue(('%s=' % item) in out) 148 | 149 | 150 | if __name__ == '__main__': 151 | tm.runtests() 152 | -------------------------------------------------------------------------------- /cicd/get-server-info.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Retrieve CAS server information from CAS log 5 | 6 | The CAS server command is expected to have a `-display` option with a unique 7 | key for the invoked server. This is used to retrieve the PID of the server 8 | process. The basename of the CAS log file must match the value in the 9 | `-display` option. 10 | 11 | ''' 12 | 13 | import argparse 14 | import os 15 | import re 16 | import subprocess 17 | import sys 18 | import time 19 | 20 | 21 | def print_err(*args, **kwargs): 22 | ''' Print a message to stderr ''' 23 | sys.stderr.write(*args, **kwargs) 24 | sys.stderr.write('\n') 25 | 26 | 27 | def main(args): 28 | ''' 29 | Main routine 30 | 31 | Parameters 32 | ---------- 33 | args : argparse arguments 34 | Arguments to the command-line 35 | 36 | ''' 37 | if not os.path.isfile(args.log_file): 38 | print_err('ERROR: File not found: {}.'.format(args.log_file)) 39 | sys.exit(1) 40 | 41 | iters = 0 42 | for i in range(args.retries): 43 | iters += 1 44 | time.sleep(args.interval) 45 | if iters > args.retries: 46 | print_err('ERROR: Could not locate CAS log file.') 47 | sys.exit(1) 48 | 49 | with open(args.log_file, 'r') as logf: 50 | txt = logf.read() 51 | m = re.search(r'===\s+.+?(\S+):(\d+)\s+.+?\s+.+?:(\d+)\s+===', txt) 52 | if m: 53 | hostname = m.group(1) 54 | binary_port = m.group(2) 55 | http_port = m.group(3) 56 | 57 | sys.stdout.write('CASHOST={} '.format(hostname)) 58 | sys.stdout.write('CAS_HOST={} '.format(hostname)) 59 | sys.stdout.write('CAS_BINARY_PORT={} '.format(binary_port)) 60 | sys.stdout.write('CAS_HTTP_PORT={} '.format(http_port)) 61 | sys.stdout.write('CAS_BINARY_URL=cas://{}:{} '.format(hostname, 62 | binary_port)) 63 | sys.stdout.write('CAS_HTTP_URL=http://{}:{} '.format(hostname, 64 | http_port)) 65 | 66 | if 'CASPROTOCOL' in os.environ or 'CAS_PROTOCOL' in os.environ: 67 | protocol = os.environ.get('CASPROTOCOL', 68 | os.environ.get('CAS_PROTOCOL', 'http')) 69 | if protocol == 'cas': 70 | sys.stdout.write('CASPROTOCOL=cas ') 71 | sys.stdout.write('CAS_PROTOCOL=cas ') 72 | sys.stdout.write('CASPORT={} '.format(binary_port)) 73 | sys.stdout.write('CAS_PORT={} '.format(binary_port)) 74 | sys.stdout.write('CASURL=cas://{}:{} '.format(hostname, 75 | binary_port)) 76 | sys.stdout.write('CAS_URL=cas://{}:{} '.format(hostname, 77 | binary_port)) 78 | else: 79 | sys.stdout.write('CASPROTOCOL={} '.format(protocol)) 80 | sys.stdout.write('CAS_PROTOCOL={} '.format(protocol)) 81 | sys.stdout.write('CASPORT={} '.format(http_port)) 82 | sys.stdout.write('CAS_PORT={} '.format(http_port)) 83 | sys.stdout.write('CASURL={}://{}:{} '.format(protocol, hostname, 84 | http_port)) 85 | sys.stdout.write('CAS_URL={}://{}:{} '.format(protocol, hostname, 86 | http_port)) 87 | 88 | elif 'REQUIRES_TK' in os.environ: 89 | if os.environ.get('REQUIRES_TK', '') == 'true': 90 | sys.stdout.write('CASPROTOCOL=cas ') 91 | sys.stdout.write('CAS_PROTOCOL=cas ') 92 | sys.stdout.write('CASPORT={} '.format(binary_port)) 93 | sys.stdout.write('CAS_PORT={} '.format(binary_port)) 94 | sys.stdout.write('CASURL=cas://{}:{} '.format(hostname, 95 | binary_port)) 96 | sys.stdout.write('CAS_URL=cas://{}:{} '.format(hostname, 97 | binary_port)) 98 | else: 99 | sys.stdout.write('CASPROTOCOL=http ') 100 | sys.stdout.write('CAS_PROTOCOL=http ') 101 | sys.stdout.write('CASPORT={} '.format(http_port)) 102 | sys.stdout.write('CAS_PORT={} '.format(http_port)) 103 | sys.stdout.write('CASURL=http://{}:{} '.format(hostname, 104 | http_port)) 105 | sys.stdout.write('CAS_URL=http://{}:{} '.format(hostname, 106 | http_port)) 107 | 108 | # Get CAS server pid 109 | cmd = ('ssh -x -o StrictHostKeyChecking=no {} ' 110 | 'ps ax | grep {} | grep -v grep | head -1' 111 | ).format(hostname, '.'.join(args.log_file.split('.')[:-1])) 112 | pid = subprocess.check_output(cmd, shell=True) \ 113 | .decode('utf-8').strip().split(' ', 1)[0] 114 | sys.stdout.write('CAS_PID={} '.format(pid)) 115 | 116 | break 117 | 118 | 119 | if __name__ == '__main__': 120 | 121 | opts = argparse.ArgumentParser(description=__doc__.strip(), 122 | formatter_class=argparse.RawTextHelpFormatter) 123 | 124 | opts.add_argument('log_file', type=str, metavar='log-file', 125 | help='path to CAS server log') 126 | 127 | opts.add_argument('--retries', '-r', default=5, type=int, metavar='#', 128 | help='number of retries in attempting to locate the log file') 129 | opts.add_argument('--interval', '-i', default=3, type=int, metavar='#', 130 | help='number of seconds between each retry') 131 | 132 | args = opts.parse_args() 133 | 134 | main(args) 135 | -------------------------------------------------------------------------------- /cicd/generate-tox-ini.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | ''' 4 | Generate a Tox config file for all test configurations 5 | 6 | To create a test matrix for all supported versions of Python, 7 | this utility takes a template `tox.ini` file and adds additional 8 | Tox environments to the end of it. The version of Python and 9 | pandas used in the environments are determined by those available 10 | on Anaconda. 11 | 12 | The versions of Python on Anaconda are intesected with the versions 13 | of Python supported in the SWAT C extensions in the package 14 | (e.g., _pyswat.so (2.7), _pyswa35.so (3.5), etc.). Pandas versions 15 | are distributed across the version of Python to increase overall 16 | coverage. 17 | 18 | ''' 19 | 20 | import argparse 21 | import glob 22 | import json 23 | import os 24 | import platform 25 | import re 26 | import subprocess 27 | import sys 28 | 29 | 30 | def print_err(*args, **kwargs): 31 | ''' Print a message to stderr ''' 32 | sys.stderr.write(*args, **kwargs) 33 | sys.stderr.write('\n') 34 | 35 | 36 | def get_platform(): 37 | ''' Return the Anaconda platform name for the current platform ''' 38 | plat = platform.system().lower() 39 | if 'darwin' in plat: 40 | return 'osx-64' 41 | if plat.startswith('win'): 42 | return 'win-64' 43 | if 'linux' in plat: 44 | machine = platform.machine().lower() 45 | if 'x86' in machine: 46 | return 'linux-64' 47 | if 'ppc' in machine: 48 | return 'linux-ppc64le' 49 | return 'unknown' 50 | 51 | 52 | def get_supported_versions(root): 53 | ''' Get the Python versions supported in the current version of SWAT ''' 54 | out = set() 55 | for ver in glob.glob(os.path.join(root, 'swat', 'lib', '*', '_py*swat*.*')): 56 | ver = re.match(r'_py(\d*)swat', os.path.basename(ver)).group(1) or '27' 57 | if (ver[0] == '2'): 58 | # print_err("get_supported_versions: skipping {}".format(ver)) 59 | continue 60 | if ((int(ver[1:]) < 7) and (ver[0] == '3')): 61 | # print_err("get_supported_versions: skipping {}".format(ver)) 62 | continue 63 | out.add('{}.{}'.format(ver[0], ver[1:])) 64 | return list(sorted(out)) 65 | 66 | 67 | def main(args): 68 | ''' Main routine ''' 69 | 70 | info = set() 71 | 72 | cmd = ['conda', 'search', '-q', '--json', 73 | '--subdir', args.platform, 'defaults::pandas'] 74 | out = subprocess.check_output(cmd) 75 | 76 | for item in json.loads(out)['pandas']: 77 | pyver = [x for x in item['depends'] if x.startswith('python')][0] 78 | pyver = re.findall(r'(\d+\.\d+)(?:\.\d+)?', pyver)[0] 79 | pdver = re.findall(r'(\d+\.\d+)(?:\.\d+)?', item['version'])[0] 80 | info.add((pyver, pdver)) 81 | 82 | supported = get_supported_versions(args.root) 83 | if not supported: 84 | print_err('ERROR: Could not determine supported versions of Python.') 85 | return 1 86 | 87 | final = {} 88 | for pyver, pdver in sorted(info): 89 | if pyver not in supported: 90 | continue 91 | pdvers = final.setdefault(pyver, set()) 92 | pdvers.add(pdver) 93 | final[pyver] = pdvers 94 | 95 | # Pick a subset of the matrix to test. Try taking unique combinations. 96 | subset = [] 97 | pyvers = list(sorted(final.keys())) 98 | 99 | # Map oldest Python version to oldest pandas version 100 | pdvers = list(sorted(final[pyvers[0]])) 101 | subset.append((pyvers[0], pdvers[0])) 102 | 103 | # Remaining Python versions get mapped to same interval of pandas version 104 | pyvers = pyvers[1:] 105 | for i in range(-len(pyvers), 0): 106 | pdvers = list(sorted(final[pyvers[i]])) 107 | try: 108 | subset.append((pyvers[i], pdvers[i])) 109 | except IndexError: 110 | subset.append((pyvers[i], pdvers[0])) 111 | 112 | if not pyvers: 113 | print_err('ERROR: No Python versions were found.') 114 | return 1 115 | 116 | if not pdvers: 117 | print_err('ERROR: No pandas versions were found.') 118 | return 1 119 | 120 | # Generate Tox configurations for testenvs 121 | for pkg in ['conda', 'pip']: 122 | out = ['', '#', '# BEGIN GENERATED ENVIRONMENTS', '#', ''] 123 | envlist = [] 124 | prev_pyver = '' 125 | for pyver, pdver in subset: 126 | if prev_pyver != pyver: 127 | out.append('#') 128 | out.append('# Python {}'.format(pyver)) 129 | out.append('#') 130 | prev_pyver = pyver 131 | 132 | name = 'py{}-{}-cicd'.format(pyver.replace('.', ''), pkg) 133 | envlist.append(name) 134 | out.append('[testenv:{}]'.format(name)) 135 | out.append('commands = {{[testenv:{}]commands}}'.format(pkg)) 136 | out.append('conda_deps =') 137 | out.append(' {[testenv]conda_deps}') 138 | out.append(' pandas=={}*'.format(pdver)) 139 | out.append('') 140 | 141 | # Write new Tox configuration 142 | with open(args.tox_ini, 'r') as tox_in: 143 | lines = iter(tox_in.readlines()) 144 | 145 | out_file = '{}-{}.ini'.format(os.path.splitext(args.tox_ini)[0], pkg) 146 | with open(out_file, 'w') as tox_out: 147 | for line in lines: 148 | # Override envlist 149 | if line.startswith('envlist'): 150 | tox_out.write('envlist =\n') 151 | for item in envlist: 152 | tox_out.write(' {}\n'.format(item)) 153 | for line in lines: 154 | if not line.startswith(' '): 155 | break 156 | tox_out.write(line) 157 | 158 | # Write new environments 159 | for item in out: 160 | tox_out.write(item) 161 | tox_out.write('\n') 162 | 163 | 164 | if __name__ == '__main__': 165 | parser = argparse.ArgumentParser(description=__doc__.strip(), 166 | formatter_class=argparse.RawTextHelpFormatter) 167 | 168 | parser.add_argument('tox_ini', type=str, metavar='ini-file', 169 | help='path to tox.ini file') 170 | 171 | parser.add_argument('--root', type=str, metavar='', default='.', 172 | help='root directory of Python package') 173 | parser.add_argument('--platform', '-p', type=str, metavar='', 174 | choices=['linux-64', 'osx-64', 'win-64', 'linux-ppc64le'], 175 | default=get_platform(), 176 | help='platform of the resulting package') 177 | 178 | args = parser.parse_args() 179 | 180 | sys.exit(main(args) or 0) 181 | -------------------------------------------------------------------------------- /swat/cas/rest/value.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | Class for receiving values from a CAS response 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | import base64 27 | from .table import REST_CASTable 28 | from ..types import blob 29 | from ...utils.compat import (a2u, int32, int64, float64, text_types, 30 | binary_types, int32_types, int64_types, 31 | float64_types, items_types) 32 | 33 | 34 | def _value2python(_value, soptions, errors, connection, 35 | ctb2tabular, b64decode, cas2python_datetime, 36 | cas2python_date, cas2python_time): 37 | ''' Convert JSON generated values to Python objects ''' 38 | if isinstance(_value, dict): 39 | if _value.get('_ctb'): 40 | return ctb2tabular(REST_CASTable(_value), soptions, connection) 41 | elif sorted(_value.keys()) == ['data', 'length']: 42 | return blob(base64.b64decode(_value['data'])) 43 | 44 | # Short circuit reflection data 45 | if 'actions' in _value and _value.get('actions', [{}])[0].get('params', False): 46 | return _value 47 | 48 | out = {} 49 | for key, value in _value.items(): 50 | out[key] = _value2python(value, soptions, errors, connection, 51 | ctb2tabular, b64decode, cas2python_datetime, 52 | cas2python_date, cas2python_time) 53 | return out 54 | 55 | if isinstance(_value, items_types): 56 | out = [] 57 | for i, value in enumerate(_value): 58 | out.append(_value2python(value, soptions, errors, connection, 59 | ctb2tabular, b64decode, cas2python_datetime, 60 | cas2python_date, cas2python_time)) 61 | return out 62 | 63 | if isinstance(_value, binary_types): 64 | return a2u(_value, 'utf-8') 65 | 66 | return _value 67 | 68 | # elif vtype == 'date': 69 | # return cas2python_date(_value) 70 | 71 | # elif vtype == 'time': 72 | # return cas2python_time(_value) 73 | 74 | # elif vtype == 'datetime': 75 | # return cas2python_datetime(_value) 76 | 77 | 78 | class REST_CASValue(object): 79 | ''' CASValue wrapper ''' 80 | 81 | def __init__(self, key, value): 82 | ''' 83 | Create a CASValue-like object 84 | 85 | Parameters 86 | ---------- 87 | key : string or int or None 88 | The key for the value 89 | value : any 90 | The value itself 91 | 92 | Returns 93 | ------- 94 | REST_CASValue object 95 | 96 | ''' 97 | self._key = key 98 | self._value = value 99 | 100 | def toPython(self, _sw_value, soptions, errors, connection, ctb2tabular, 101 | b64decode, cas2python_datetime, cas2python_date, cas2python_time): 102 | ''' Convert a CAS value to Python ''' 103 | return _value2python(self._value, soptions, errors, connection, ctb2tabular, 104 | b64decode, cas2python_datetime, cas2python_date, 105 | cas2python_time) 106 | 107 | def getTypeName(self): 108 | ''' Get the object type ''' 109 | return 'value' 110 | 111 | def getSOptions(self): 112 | ''' Get the SOptions value ''' 113 | return '' 114 | 115 | def isNULL(self): 116 | ''' Is this a NULL object? ''' 117 | return False 118 | 119 | def hasKeys(self): 120 | ''' Does the value have keys? ''' 121 | return isinstance(self._value, dict) and self._value 122 | 123 | def getType(self): 124 | ''' Get the value type ''' 125 | _value = self._value 126 | if isinstance(_value, float64_types): 127 | return 'double' 128 | if isinstance(_value, text_types): 129 | return 'string' 130 | if isinstance(_value, int32_types): 131 | return 'int32' 132 | if isinstance(_value, int64_types): 133 | return 'int64' 134 | if isinstance(_value, binary_types): 135 | return 'string' 136 | if isinstance(_value, items_types): 137 | return 'list' 138 | if isinstance(_value, dict): 139 | if _value.get('_ctb'): 140 | return 'table' 141 | return 'list' 142 | if _value is None: 143 | return 'nil' 144 | raise TypeError('%s: %s' % (self._key, _value)) 145 | 146 | def getKey(self): 147 | ''' Get the value's key ''' 148 | return self._key 149 | 150 | def getInt32(self): 151 | ''' Get the value as an int32 ''' 152 | return int32(self._value) 153 | 154 | def getInt64(self): 155 | ''' Get the value as an int64 ''' 156 | return int64(self._value) 157 | 158 | def getDouble(self): 159 | ''' Get the value as a double ''' 160 | return float64(self._value) 161 | 162 | def getString(self): 163 | ''' Get the value as a string ''' 164 | return a2u(self._value, 'utf-8') 165 | 166 | def getBoolean(self): 167 | ''' Get the value as a boolean ''' 168 | return self._value and True or False 169 | 170 | def getList(self): 171 | ''' Get the value as a list ''' 172 | return self._value 173 | 174 | def getListNItems(self): 175 | ''' Get the number of items in the value's list ''' 176 | return len(self._value) 177 | 178 | def getListItem(self, i): 179 | ''' Get a specific list item ''' 180 | if isinstance(self._value, dict): 181 | if not hasattr(self, '_items'): 182 | self._items = list(sorted(self._value.items())) 183 | return REST_CASValue(*self._items[i]) 184 | return REST_CASValue(None, self._value[i]) 185 | 186 | def getTable(self): 187 | ''' Get the value as a table ''' 188 | return REST_CASTable(self._value) 189 | 190 | def getLastErrorMessage(self): 191 | ''' Retrieve any queued error messages ''' 192 | return '' 193 | -------------------------------------------------------------------------------- /swat/utils/args.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ''' 20 | Utilities for dealing with function/method arguments 21 | 22 | ''' 23 | 24 | from __future__ import print_function, division, absolute_import, unicode_literals 25 | 26 | import re 27 | import six 28 | import locale as loc 29 | from .compat import items_types 30 | 31 | 32 | def mergedefined(*args): 33 | ''' 34 | Merge the defined key/value pairs of multiple dictionaries 35 | 36 | Parameters 37 | ---------- 38 | *args : list of dicts 39 | One or more dictionaries 40 | 41 | Returns 42 | ------- 43 | dict 44 | Dictionary of merged key/value pairs 45 | 46 | ''' 47 | out = {} 48 | for kwarg in args: 49 | for key, value in six.iteritems(kwarg): 50 | if value is not None: 51 | out[key] = value 52 | return out 53 | 54 | 55 | def dict2kwargs(dct, fmt='dict(%s)', nestfmt='dict(%s)', ignore=None): 56 | ''' 57 | Create a string from a dict-like object using keyword argument syntax 58 | 59 | Parameters 60 | ---------- 61 | dct : dict-like object 62 | The dictionary to represent as a string 63 | fmt : string, optional 64 | The format string to use at the top level 65 | nestfmt : string, optional 66 | The format string to use for nested dictionaries 67 | ignore : list 68 | Keys to ignore at the top level 69 | 70 | Returns 71 | ------- 72 | string 73 | String representation of a dict 74 | 75 | ''' 76 | if ignore is None: 77 | ignore = [] 78 | out = [] 79 | for key, value in sorted(six.iteritems(dct)): 80 | if key in ignore: 81 | continue 82 | if isinstance(value, dict): 83 | out.append('%s=%s' % (key, dict2kwargs(value, fmt=nestfmt))) 84 | elif isinstance(value, items_types): 85 | sublist = [] 86 | for item in value: 87 | if isinstance(item, (dict, items_types)): 88 | sublist.append(dict2kwargs(item)) 89 | else: 90 | sublist.append(repr(item)) 91 | if isinstance(value, tuple): 92 | fmtstr = '%s=(%s)' 93 | elif isinstance(value, set): 94 | fmtstr = '%s={%s}' 95 | else: 96 | fmtstr = '%s=[%s]' 97 | out.append(fmtstr % (key, ', '.join(sublist))) 98 | else: 99 | out.append('%s=%s' % (key, repr(value))) 100 | return fmt % ', '.join(out) 101 | 102 | 103 | def getsoptions(**kwargs): 104 | ''' 105 | Convert keyword arguments to soptions format 106 | 107 | Paramaters 108 | ---------- 109 | **kwargs : any, optional 110 | Arbitrary keyword arguments 111 | 112 | Returns 113 | ------- 114 | string 115 | Formatted string of all options 116 | 117 | ''' 118 | soptions = [] 119 | for key, value in six.iteritems(kwargs): 120 | if value is None: 121 | continue 122 | if key == 'locale': 123 | value = getlocale(**dict(locale=value)) 124 | soptions.append('%s=%s' % (key, value)) 125 | return ' '.join(soptions) 126 | 127 | 128 | def parsesoptions(soptions): 129 | ''' 130 | Convert soptions string to dictionary 131 | 132 | Parameters 133 | ---------- 134 | soptions : string 135 | Formatted string of options 136 | 137 | Returns 138 | ------- 139 | dict 140 | 141 | ''' 142 | out = {} 143 | if not soptions: 144 | return out 145 | soptions = soptions.strip() 146 | if not soptions: 147 | return out 148 | while re.match(r'^[\w+-]+\s*=', soptions): 149 | name, soptions = re.split(r'\s*=\s*', soptions, 1) 150 | if soptions.startswith('{'): 151 | match = re.match(r'^\{\s*([^\}]*)\s*\}\s*(.*)$', soptions) 152 | value = re.split(r'\s+', match.group(1)) 153 | soptions = match.group(2) or '' 154 | elif ' ' in soptions: 155 | value, soptions = re.split(r'\s+', soptions, 1) 156 | else: 157 | value = soptions 158 | soptions = '' 159 | out[name] = value 160 | return out 161 | 162 | 163 | def getlocale(locale=None): 164 | ''' 165 | Get configured language code for locale 166 | 167 | If a locale argument is specified, that code is returned. 168 | Otherwise, Python's locale module is used to acquire 169 | the currently configured language code. 170 | 171 | Parameters 172 | ---------- 173 | locale : string, optional 174 | POSIX language code 175 | 176 | Returns 177 | ------- 178 | string 179 | String containing locale 180 | 181 | ''' 182 | if locale: 183 | return locale 184 | locale = loc.getlocale()[0] 185 | if locale: 186 | return locale 187 | return loc.getdefaultlocale()[0] 188 | 189 | 190 | def iteroptions(*args, **kwargs): 191 | ''' 192 | Iterate through name / value pairs of options 193 | 194 | Options can come in several forms. They can be consecutive arguments 195 | where the first argument is the name and the following argument is 196 | the value. They can be two-element tuples (or lists) where the first 197 | element is the name and the second element is the value. You can 198 | also pass in a dictionary of key / value pairs. And finally, you can 199 | use keyword arguments. 200 | 201 | Parameters 202 | ---------- 203 | *args : any, optional 204 | See description above. 205 | **kwargs : key / value pairs, optional 206 | Arbitrary keyword arguments. 207 | 208 | Returns 209 | ------- 210 | generator 211 | Each iteration returns a name / value pair in a tuple 212 | 213 | ''' 214 | args = list(args) 215 | while args: 216 | item = args.pop(0) 217 | if isinstance(item, (list, tuple)): 218 | yield item[0], item[1] 219 | elif isinstance(item, dict): 220 | for key, value in six.iteritems(item): 221 | yield key, value 222 | else: 223 | yield item, args.pop(0) 224 | for key, value in six.iteritems(kwargs): 225 | yield key, value 226 | -------------------------------------------------------------------------------- /swat/tests/cas/test_datetime.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # encoding: utf-8 3 | # 4 | # Copyright SAS Institute 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the License); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # http://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | import datetime 20 | import pandas as pd 21 | import numpy as np 22 | import os 23 | import re 24 | import six 25 | import swat 26 | import swat.utils.testing as tm 27 | import sys 28 | import unittest 29 | from swat.cas.utils.datetime import \ 30 | (str2cas_timestamp, cas2python_timestamp, 31 | str2cas_date, cas2python_date, cas2sas_date, str2cas_time, cas2python_time, 32 | cas2sas_time, python2cas_datetime, python2cas_date, python2cas_time, 33 | str2sas_timestamp, sas2python_timestamp, sas2cas_timestamp, str2sas_date, 34 | sas2python_date, sas2python_time, python2sas_datetime, python2sas_date, 35 | python2sas_time, cas2sas_timestamp, sas2cas_date, str2sas_time, sas2cas_time) 36 | 37 | from swat.utils.compat import patch_pandas_sort 38 | from swat.utils.testing import UUID_RE, get_cas_host_type, load_data 39 | 40 | patch_pandas_sort() 41 | 42 | # Pick sort keys that will match across SAS and Pandas sorting orders 43 | SORT_KEYS = ['Origin', 'MSRP', 'Horsepower', 'Model'] 44 | 45 | USER, PASSWD = tm.get_user_pass() 46 | HOST, PORT, PROTOCOL = tm.get_host_port_proto() 47 | 48 | 49 | class TestDateTime(tm.TestCase): 50 | 51 | server_type = None 52 | 53 | def setUp(self): 54 | swat.reset_option() 55 | swat.options.cas.print_messages = False 56 | swat.options.interactive_mode = True 57 | swat.options.cas.missing.int64 = -999999 58 | 59 | self.s = swat.CAS(HOST, PORT, USER, PASSWD, protocol=PROTOCOL) 60 | 61 | if type(self).server_type is None: 62 | type(self).server_type = get_cas_host_type(self.s) 63 | 64 | self.srcLib = tm.get_casout_lib(self.server_type) 65 | 66 | self.dates = load_data( 67 | self.s, 'datasources/dates.csv', self.server_type, 68 | importoptions=dict(vars=dict(Region=dict(), 69 | Date=dict(format='DATE'))))['casTable'] 70 | self.dates = self.dates.to_frame().set_index('Region') 71 | 72 | self.datetimes = load_data( 73 | self.s, 'datasources/datetimes.csv', self.server_type, 74 | importoptions=dict(vars=dict(Region=dict(), 75 | Datetime=dict(format='DATETIME'))))['casTable'] 76 | self.datetimes = self.datetimes.to_frame().set_index('Region') 77 | 78 | def tearDown(self): 79 | # tear down tests 80 | self.s.endsession() 81 | del self.s 82 | swat.reset_option() 83 | 84 | def test_cas_datetime(self): 85 | self.assertEqual(str2cas_timestamp('19700101T12:00'), 315662400000000) 86 | self.assertEqual(cas2python_timestamp(315662400000000), 87 | datetime.datetime(1970, 1, 1, 12, 0, 0)) 88 | self.assertEqual(cas2sas_timestamp(315662400000000), 315662400) 89 | 90 | self.assertEqual(str2cas_date('19700101T12:00'), 3653) 91 | self.assertEqual(cas2python_date(3653), 92 | datetime.date(1970, 1, 1)) 93 | self.assertEqual(cas2sas_date(3653), 3653) 94 | 95 | self.assertEqual(str2cas_time('19700101T12:00'), 43200000000) 96 | self.assertEqual(cas2python_time(43200000000), 97 | datetime.time(12, 0, 0)) 98 | self.assertEqual(cas2sas_time(43200000000), 43200) 99 | 100 | def test_python2cas(self): 101 | self.assertEqual(python2cas_datetime(datetime.datetime(1970, 1, 1, 12, 0, 0)), 102 | 315662400000000) 103 | self.assertEqual(python2cas_date(datetime.date(1970, 1, 1)), 104 | 3653) 105 | self.assertEqual(python2cas_date(datetime.datetime(1970, 1, 1, 12, 0, 0)), 106 | 3653) 107 | # self.assertEqual(python2cas_date(datetime.time(12, 0, 0)), 108 | # 3653) 109 | self.assertEqual(python2cas_time(datetime.time(12, 0)), 110 | 43200000000) 111 | 112 | def test_sas_datetime(self): 113 | self.assertEqual(str2sas_timestamp('19700101T12:00'), 315662400) 114 | self.assertEqual(sas2python_timestamp(315662400), 115 | datetime.datetime(1970, 1, 1, 12, 0, 0)) 116 | self.assertEqual(sas2cas_timestamp(315662400), 315662400000000) 117 | 118 | self.assertEqual(str2sas_date('19700101T12:00'), 3653) 119 | self.assertEqual(sas2python_date(3653), 120 | datetime.date(1970, 1, 1)) 121 | self.assertEqual(sas2cas_date(3653), 3653) 122 | 123 | self.assertEqual(str2sas_time('19700101T12:00'), 43200) 124 | self.assertEqual(sas2python_time(43200), 125 | datetime.time(12, 0, 0)) 126 | self.assertEqual(sas2cas_time(43200), 43200000000) 127 | 128 | def test_python2sas(self): 129 | self.assertEqual(python2sas_datetime(datetime.datetime(1970, 1, 1, 12, 0, 0)), 130 | 315662400) 131 | self.assertEqual(python2sas_date(datetime.date(1970, 1, 1)), 132 | 3653) 133 | self.assertEqual(python2sas_date(datetime.datetime(1970, 1, 1, 12, 0, 0)), 134 | 3653) 135 | # self.assertEqual(python2sas_date(datetime.time(12, 0, 0)), 136 | # 3653) 137 | self.assertEqual(python2sas_time(datetime.time(12, 0)), 138 | 43200) 139 | 140 | def test_sas_date_conversion(self): 141 | self.assertEqual(self.dates.loc['N', 'Date'], datetime.date(1960, 1, 21)) 142 | self.assertEqual(self.dates.loc['S', 'Date'], datetime.date(1960, 1, 31)) 143 | self.assertEqual(self.dates.loc['E', 'Date'], datetime.date(1960, 10, 27)) 144 | self.assertEqual(self.dates.loc['W', 'Date'], datetime.date(1961, 2, 4)) 145 | self.assertTrue(pd.isnull(self.dates.loc['X', 'Date'])) 146 | 147 | def test_sas_datetime_conversion(self): 148 | self.assertEqual(self.datetimes.loc['N', 'Datetime'], 149 | datetime.datetime(2000, 3, 17, 0, 0, 0)) 150 | self.assertEqual(self.datetimes.loc['S', 'Datetime'], 151 | datetime.datetime(1991, 10, 17, 14, 45, 32)) 152 | self.assertEqual(self.datetimes.loc['E', 'Datetime'], 153 | datetime.datetime(1960, 1, 1, 0, 0, 0)) 154 | self.assertEqual(self.datetimes.loc['W', 'Datetime'], 155 | datetime.datetime(1959, 12, 31, 23, 59, 59)) 156 | self.assertTrue(pd.isnull(self.datetimes.loc['X', 'Datetime'])) 157 | 158 | 159 | if __name__ == '__main__': 160 | tm.runtests() 161 | --------------------------------------------------------------------------------