├── 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('
= 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*)',
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
"
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 |
--------------------------------------------------------------------------------