diff --git a/.github/workflows/pull_request.yml b/.github/workflows/pull_request.yml index 4ca8032f..aa2f2cc3 100644 --- a/.github/workflows/pull_request.yml +++ b/.github/workflows/pull_request.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: - python-version: [3.6, 3.7] + python-version: [3.6, 3.7, 3.8] os: [ubuntu-latest] steps: @@ -24,10 +24,11 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt + pip install -r requirements_test.txt + pip install -r requirements_dev.txt - name: Lint with flake8 run: | - pip install flake8 # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide @@ -35,11 +36,9 @@ jobs: - name: Test with pytest run: | - pip install pytest pytest-cov coveralls coverage==4.5.4 - pytest tests/unit --cov=tabpy --cov-append - pytest tests/integration --cov=tabpy --cov-append + pytest tests --cov=tabpy coveralls - env: + env: COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} - name: Markdownlint @@ -53,7 +52,7 @@ jobs: strategy: matrix: - python-version: [3.7] + python-version: [3.7, 3.8] os: [windows-latest] steps: @@ -68,12 +67,11 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt + pip install -r requirements_test.txt - name: Test with pytest run: | - pip install pytest pytest-cov coveralls - pytest tests/unit - pytest tests/integration + pytest tests mac-build: name: ${{ matrix.python-version }} on ${{ matrix.os }} @@ -81,7 +79,7 @@ jobs: strategy: matrix: - python-version: [3.7] + python-version: [3.7, 3.8] os: [macos-latest] steps: @@ -96,10 +94,9 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt + pip install -r requirements_test.txt - name: Test with pytest run: | - pip install pytest pytest-cov coveralls - pytest tests/unit - pytest tests/integration + pytest tests \ No newline at end of file diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index d3f88810..08874122 100755 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: - python-version: [3.6, 3.7] + python-version: [3.6, 3.7, 3.8] os: [ubuntu-latest] steps: @@ -24,10 +24,11 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt + pip install -r requirements_test.txt + pip install -r requirements_dev.txt - name: Lint with flake8 run: | - pip install flake8 # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide @@ -35,11 +36,9 @@ jobs: - name: Test with pytest run: | - pip install pytest pytest-cov coveralls coverage==4.5.4 - pytest tests/unit --cov=tabpy --cov-append - pytest tests/integration --cov=tabpy --cov-append + pytest tests --cov=tabpy coveralls - env: + env: COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} - name: Markdownlint @@ -53,7 +52,7 @@ jobs: strategy: matrix: - python-version: [3.7] + python-version: [3.7, 3.8] os: [windows-latest] steps: @@ -68,12 +67,11 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt + pip install -r requirements_test.txt - name: Test with pytest run: | - pip install pytest pytest-cov coveralls - pytest tests/unit - pytest tests/integration + pytest tests mac-build: name: ${{ matrix.python-version }} on ${{ matrix.os }} @@ -81,7 +79,7 @@ jobs: strategy: matrix: - python-version: [3.7] + python-version: [3.7, 3.8] os: [macos-latest] steps: @@ -96,10 +94,9 @@ jobs: run: | python -m pip install --upgrade pip pip install -r requirements.txt + pip install -r requirements_test.txt - name: Test with pytest run: | - pip install pytest pytest-cov coveralls - pytest tests/unit - pytest tests/integration + pytest tests \ No newline at end of file diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 22572143..01d763a1 100755 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -1,18 +1,31 @@ build: - environment: - python: 3.6 - nodes: - analysis: - project_setup: - override: - - pip install sklearn pandas numpy textblob nltk scipy - tests: - override: - - py-scrutinizer-run - - - command: pylint-run - use_website_config: true - tests: true + environment: + python: 3.6 + tests: + before: + - pip install pytest pytest-cov coverage + - pip install -r requirements.txt + nodes: + analysis: + project_setup: + override: + - pip install sklearn pandas numpy textblob nltk scipy + tests: + override: + - py-scrutinizer-run + - command: pylint-run + use_website_config: true + coverage: + tests: + before: + - pip install pytest pytest-cov coverage coveralls + - pip install -r requirements.txt + override: + - command: pytest tests --cov=tabpy + coverage: + file: '.coverage' + config_file: '.coveragerc' + format: 'py-cc' checks: python: code_rating: true diff --git a/CHANGELOG b/CHANGELOG index eed5b36b..98a05232 100755 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,19 +1,21 @@ # Changelog -## v0.8.9 +## v0.8.10 ### Improvements -- Minor code improvements. +- TabPy works with Python 3.8 now. - Documentation updates with referencing Tableau Help pages. - Added Client.remove() method for deleting deployed models. + ### Bug Fixes - Fixed failing Ctrl+C handler. - Fixed query_timeout bug. - Fixed None in result collection bug. - Fixed script evaluation with missing result/return bug. +- Fixed startup failure on Windows for Python 3.8. ## v0.8.9 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ba332aea..d51507fa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,13 +34,6 @@ be able to work on TabPy changes: - Python 3.6 or 3.7: - To see which version of Python you have installed, run `python --version`. - git -- TabPy repo: - - Get the latest TabPy repository with - `git clone https://github.com/tableau/TabPy.git`. - - Create a new branch for your changes. - - When changes are ready push them on github and create merge request. -- PIP packages - install all with - `pip install pytest flake8 twine coverage --upgrade` command - Node.js for npm packages - install from . - NPM packages - install all with `npm install markdown-toc markdownlint` command. @@ -62,6 +55,8 @@ be able to work on TabPy changes: ```sh python -m pip install --upgrade pip pip install -r requirements.txt + pip install -r requirements_dev.txt + pip install -r requirements_test.txt ``` ## Tests @@ -94,7 +89,7 @@ You can run unit tests to collect code coverage data. To do so run `pytest` either for server or tools test, or even combined: ```sh -pytest tests --cov=tabpy-server/tabpy_server --cov=tabpy-tools/tabpy_tools --cov-append +pytest tests --cov=tabpy ``` ## TabPy in Python Virtual Environment diff --git a/requirements_dev.txt b/requirements_dev.txt new file mode 100755 index 00000000..1ae3ec61 --- /dev/null +++ b/requirements_dev.txt @@ -0,0 +1 @@ +flake8 \ No newline at end of file diff --git a/requirements_test.txt b/requirements_test.txt new file mode 100755 index 00000000..2bbfacd9 --- /dev/null +++ b/requirements_test.txt @@ -0,0 +1,3 @@ +coveralls +pytest +pytest-cov \ No newline at end of file diff --git a/setup.py b/setup.py index 70c846e0..cdafa7e0 100755 --- a/setup.py +++ b/setup.py @@ -79,7 +79,7 @@ def read(fname): "singledispatch", "six", "tornado", - "urllib3<1.25,>=1.21.1", + "urllib3", ], entry_points={ "console_scripts": [ diff --git a/tabpy/tabpy_server/app/app.py b/tabpy/tabpy_server/app/app.py index ac98ed65..0ee807ad 100644 --- a/tabpy/tabpy_server/app/app.py +++ b/tabpy/tabpy_server/app/app.py @@ -6,6 +6,7 @@ import os import shutil import signal +import sys import tabpy.tabpy_server from tabpy.tabpy import __version__ from tabpy.tabpy_server.app.ConfigParameters import ConfigParameters @@ -30,6 +31,25 @@ logger = logging.getLogger(__name__) +def _init_asyncio_patch(): + """ + Select compatible event loop for Tornado 5+. + As of Python 3.8, the default event loop on Windows is `proactor`, + however Tornado requires the old default "selector" event loop. + As Tornado has decided to leave this to users to set, MkDocs needs + to set it. See https://github.com/tornadoweb/tornado/issues/2608. + """ + if sys.platform.startswith("win") and sys.version_info >= (3, 8): + import asyncio + try: + from asyncio import WindowsSelectorEventLoopPolicy + except ImportError: + pass # Can't assign a policy which doesn't exist. + else: + if not isinstance(asyncio.get_event_loop_policy(), WindowsSelectorEventLoopPolicy): + asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy()) + + class TabPyApp: """ TabPy application class for keeping context like settings, state, etc. @@ -113,6 +133,7 @@ def try_exit(self): ) # initialize Tornado application + _init_asyncio_patch() application = TabPyTornadoApp( [ # skip MainHandler to use StaticFileHandler .* page requests and @@ -243,7 +264,8 @@ def _parse_config(self, config_file): (SettingsParameters.KeyFile, ConfigParameters.TABPY_KEY_FILE, None), (SettingsParameters.StateFilePath, ConfigParameters.TABPY_STATE_PATH, os.path.join(pkg_path, "tabpy_server")), - (SettingsParameters.StaticPath, ConfigParameters.TABPY_STATIC_PATH, "./"), + (SettingsParameters.StaticPath, ConfigParameters.TABPY_STATIC_PATH, + os.path.join(pkg_path, "tabpy_server", "static")), (ConfigParameters.TABPY_PWD_FILE, ConfigParameters.TABPY_PWD_FILE, None), (SettingsParameters.LogRequestContext, ConfigParameters.TABPY_LOG_DETAILS, "false"), diff --git a/tabpy/tabpy_server/static/index.html b/tabpy/tabpy_server/static/index.html index 7f2da21d..37088552 100644 --- a/tabpy/tabpy_server/static/index.html +++ b/tabpy/tabpy_server/static/index.html @@ -8,19 +8,64 @@ Tableau Python Server - + + - - - +

TabPy Server Info:

+

+

Deployed Models:

+

- \ No newline at end of file +

Useful links:

+ + + + diff --git a/tests/integration/test_url.py b/tests/integration/test_url.py index 62fd3516..0fa4c03c 100755 --- a/tests/integration/test_url.py +++ b/tests/integration/test_url.py @@ -17,3 +17,10 @@ def test_notexistent_url(self): res = conn.getresponse() self.assertEqual(404, res.status) + + def test_static_page(self): + conn = self._get_connection() + conn.request("GET", "/") + res = conn.getresponse() + + self.assertEqual(200, res.status) diff --git a/tests/unit/server_tests/__init__.py b/tests/unit/server_tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/tests/unit/server_tests/test_endpoint_handler.py b/tests/unit/server_tests/test_endpoint_handler.py index 4f6e9785..40287371 100755 --- a/tests/unit/server_tests/test_endpoint_handler.py +++ b/tests/unit/server_tests/test_endpoint_handler.py @@ -1,5 +1,6 @@ import base64 import os +import sys import tempfile from tabpy.tabpy_server.app.app import TabPyApp @@ -7,9 +8,29 @@ from tornado.testing import AsyncHTTPTestCase +def _init_asyncio_patch(): + """ + Select compatible event loop for Tornado 5+. + As of Python 3.8, the default event loop on Windows is `proactor`, + however Tornado requires the old default "selector" event loop. + As Tornado has decided to leave this to users to set, MkDocs needs + to set it. See https://github.com/tornadoweb/tornado/issues/2608. + """ + if sys.platform.startswith("win") and sys.version_info >= (3, 8): + import asyncio + try: + from asyncio import WindowsSelectorEventLoopPolicy + except ImportError: + pass # Can't assign a policy which doesn't exist. + else: + if not isinstance(asyncio.get_event_loop_policy(), WindowsSelectorEventLoopPolicy): + asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy()) + + class TestEndpointHandlerWithAuth(AsyncHTTPTestCase): @classmethod def setUpClass(cls): + _init_asyncio_patch() prefix = "__TestEndpointHandlerWithAuth_" # create password file cls.pwd_file = tempfile.NamedTemporaryFile( diff --git a/tests/unit/server_tests/test_evaluation_plane_handler.py b/tests/unit/server_tests/test_evaluation_plane_handler.py index 408c6a3c..49b67dfb 100755 --- a/tests/unit/server_tests/test_evaluation_plane_handler.py +++ b/tests/unit/server_tests/test_evaluation_plane_handler.py @@ -130,10 +130,6 @@ def test_valid_creds_pass(self): ) self.assertEqual(200, response.code) - def test_null_request(self): - response = self.fetch("") - self.assertEqual(404, response.code) - def test_script_not_present(self): response = self.fetch( "/evaluate", diff --git a/tests/unit/tools_tests/test_rest.py b/tests/unit/tools_tests/test_rest.py index 59f74234..33a78bf2 100644 --- a/tests/unit/tools_tests/test_rest.py +++ b/tests/unit/tools_tests/test_rest.py @@ -18,14 +18,14 @@ def test_init_with_session(self): self.assertIs(session, rnw.session) def mock_response(self, status_code): - response = Mock(requests.Response()) + response = Mock(requests.Response) response.json.return_value = "json" response.status_code = status_code return response def setUp(self): - session = Mock(requests.session()) + session = Mock(requests.Session) session.get.return_value = self.mock_response(200) session.post.return_value = self.mock_response(200) session.put.return_value = self.mock_response(200)