diff --git a/.travis.yml b/.travis.yml index 9136aa64..669de4a0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,8 +8,9 @@ install: - pip install coveralls - npm install -g markdownlint-cli script: - - pytest tabpy-server/server_tests/ --cov=tabpy-server/tabpy_server - - pytest tabpy-tools/tools_tests/ --cov=tabpy-tools/tabpy_tools --cov-append + - export PYTHONPATH=./tabpy-server:./tabpy-tools:$PYTHONPATH + - pytest tests/unit --cov=tabpy-server/tabpy_server --cov=tabpy-tools/tabpy_tools --cov-append + - pytest tests/integration - markdownlint . after_success: - coveralls diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100755 index 00000000..a5ebf5c5 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: General", + "type": "python", + "request": "launch", + "program": "${file}", + "console": "externalTerminal", + "env": {"${PYTHONTPATH}": "${PYTHONPATH};${workspaceRoot}/tabpy-server;${workspaceRoot}/tabpy-tools"} + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 917bf975..1f1c7bb0 100755 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,9 +2,19 @@ "git.enabled": true, "files.exclude": { "**/__pycache__": true, - "**/.pytest_cache": true + "**/.pytest_cache": true, + "**/*.egg-info": true, + "**/*.pyc": true }, "python.linting.pylintEnabled": false, - "python.linting.flake8Enabled": true, - "python.linting.enabled": true + "python.linting.flake8Enabled": false, + "python.linting.enabled": true, + "python.testing.autoTestDiscoverOnSaveEnabled": true, + "python.testing.pyTestArgs": [ + "tests" + ], + "python.testing.unittestEnabled": true, + "python.testing.nosetestsEnabled": false, + "python.testing.pyTestEnabled": true, + "python.linting.pep8Enabled": true } \ No newline at end of file diff --git a/CHANGELOG b/CHANGELOG index db48fbe1..cdd9a4c0 100755 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). - Added request context logging as a feature controlled with TABPY_LOG_DETAILS configuration setting. +- Updated documentation for /info method +- Added integration tests ## v0.4 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4abb302e..d679edec 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,8 +4,12 @@ - [Environment Setup](#environment-setup) - [Prerequisites](#prerequisites) -- [Windows Specific Steps](#windows-specific-steps) -- [Linux and Mac Specific Steps](#linux-and-mac-specific-steps) +- [Cloning TabPy Repository](#cloning-tabpy-repository) +- [Setting Up Environment](#setting-up-environment) +- [Unit Tests](#unit-tests) +- [Integration Tests](#integration-tests) +- [Code Coverage](#code-coverage) +- [TabPy in Python Virtual Environment](#tabpy-in-python-virtual-environment) - [Documentation Updates](#documentation-updates) - [TabPy with Swagger](#tabpy-with-swagger) - [Code styling](#code-styling) @@ -30,10 +34,10 @@ be able to work on TabPy changes: - Create a new branch for your changes. - When changes are ready push them on github and create merge request. -## Windows Specific Steps +## Cloning TabPy Repository -1. Open a windows command prompt. -2. In the command prompt, navigate to the folder in which you would like to save +1. Open your OS shell. +2. Navigate to the folder in which you would like to save your local TabPy repository. 3. In the command prompt, enter the following commands: @@ -42,36 +46,55 @@ be able to work on TabPy changes: cd TabPy ``` -To start a local TabPy instance: +## Setting Up Environment + +Before making any code changes run environment setup script. For +Windows the next command from the repository root folder: ```sh -startup.cmd +utils\set_env.cmd ``` -To run the unit test suite: +and for Linux or Mac the next command from the repository root folder: ```sh -python tests\runtests.py +utils/set_env.sh ``` -Alternatively you can run unit tests to collect code coverage data. First -install `pytest`: +## Unit Tests + +TabPy has test suites for `tabpy-server` and `tabpy-tools` components. +To run the unit test use `pytest` which you may need to install first +(see [https://docs.pytest.org](https://docs.pytest.org) for details): ```sh -pip install pytest +pytest tests/unit ``` -And then run `pytest` either for server or tools test, or even combined: +Check `pytest` documentation for how to run individual tests or set of tests. + +## Integration Tests + +Integration tests can be executed with the next command: + +```sh +pytest tests/integration +``` + +## Code Coverage + +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 tabpy-server/server_tests/ --cov=tabpy-server/tabpy_server -pytest tabpy-tools/tools_tests/ --cov=tabpy-tools/tabpy_tools --cov-append +pytest tests --cov=tabpy-server/tabpy_server --cov=tabpy-tools/tabpy_tools --cov-append ``` -## Linux and Mac Specific Steps +## TabPy in Python Virtual Environment -If you have downloaded Tabpy and would like to manually install Tabpy Server -not using pip then follow the steps below [to run TabPy in Python virtual environment](docs/tabpy-virtualenv.md). +It is possible (and recommended) to run TabPy in a virtual environment, more +details are on +[TabPy in Python virtual environment](docs/tabpy-virtualenv.md) page. ## Documentation Updates diff --git a/docs/api-v1.md b/docs/api-v1.md new file mode 100755 index 00000000..2ba1f6e1 --- /dev/null +++ b/docs/api-v1.md @@ -0,0 +1,280 @@ +# TabPy API v1 + + + + + +- [Authentication](#authentication) +- [http:get:: /status](#httpget-status) +- [http:get:: /endpoints](#httpget-endpoints) +- [http:get:: /endpoints/:endpoint](#httpget-endpointsendpoint) +- [http:post:: /evaluate](#httppost-evaluate) +- [http:post:: /query/:endpoint](#httppost-queryendpoint) + + + + + +## Authentication + +When authentication feature is enabled for v1 API [`/info` call](server-rest.md#get-info) +response contains authentication feature parameters, e.g.: + + ```json + { + "description": "", + "creation_time": "0", + "state_path": "e:\\dev\\TabPy\\tabpy-server\\tabpy_server", + "server_version": "0.4.1", + "name": "TabPy Server", + "versions": { + "v1": { + "features": { + "authentication": { + "required": true, + "methods": { + "basic-auth": {} + } + } + } + } + } + } + ``` + +v1 authentication specific features (see the example above): + + + +Property | Description +--- | --- +`required` | Authentication is never optional for client to use if it is mentioned in features list. +`methods` | List of supported authentication methods with their properties. +`methods.basic-auth` | TabPy requires to use basic access authentication, see [TabPy Server Configuration Instructions](server-config.md#authentication) for how to configure authentication. + + + +## http:get:: /status + +Gets runtime status of deployed endpoints. If no endpoints are deployed in +the server, the returned data is an empty JSON object. + +Example request: + +```HTTP +GET /status HTTP/1.1 +Host: localhost:9004 +Accept: application/json +``` + +Example response: + +```HTTP +HTTP/1.1 200 OK +Content-Type: application/json + +{"clustering": { + "status": "LoadSuccessful", + "last_error": null, + "version": 1, + "type": "model"}, + "add": { + "status": "LoadSuccessful", + "last_error": null, + "version": 1, + "type": "model"} +} +``` + +Using curl: + +```bash +curl -X GET http://localhost:9004/status +``` + +## http:get:: /endpoints + +Gets a list of deployed endpoints and their static information. If no +endpoints are deployed in the server, the returned data is an empty JSON object. + +Example request: + +```HTTP +GET /endpoints HTTP/1.1 +Host: localhost:9004 +Accept: application/json +``` + +Example response: + +```HTTP +HTTP/1.1 200 OK +Content-Type: application/json + +{"clustering": + {"description": "", + "docstring": "-- no docstring found in query function --", + "creation_time": 1469511182, + "version": 1, + "dependencies": [], + "last_modified_time": 1469511182, + "type": "model", + "target": null}, +"add": { + "description": "", + "docstring": "-- no docstring found in query function --", + "creation_time": 1469505967, + "version": 1, + "dependencies": [], + "last_modified_time": 1469505967, + "type": "model", + "target": null} +} +``` + +Using curl: + +```bash +curl -X GET http://localhost:9004/endpoints +``` + +## http:get:: /endpoints/:endpoint + +Gets the description of a specific deployed endpoint. The endpoint must first +be deployed in the server (see the [TabPy Tools documentation](tabpy-tools.md)). + +Example request: + +```HTTP +GET /endpoints/add HTTP/1.1 +Host: localhost:9004 +Accept: application/json +``` + +Example response: + +```HTTP +HTTP/1.1 200 OK +Content-Type: application/json + +{"description": "", "docstring": "-- no docstring found in query function --", + "creation_time": 1469505967, "version": 1, "dependencies": [], + "last_modified_time": 1469505967, "type": "model", "target": null} +``` + +Using curl: + +```bash +curl -X GET http://localhost:9004/endpoints/add +``` + +## http:post:: /evaluate + +Executes a block of Python code, replacing named parameters with their provided +values. + +The expected POST body is a JSON dictionary with two elements: + +- A key `data` with a value that contains the parameter values passed to the + code. These values are key-value pairs, following a specific convention for + key names (`_arg1`, `_arg2`, etc.). +- A key `script` with a value that contains the Python code (one or more lines). + Any references to the parameter names will be replaced by their values + according to `data`. + +Example request: + +```HTTP +POST /evaluate HTTP/1.1 +Host: localhost:9004 +Accept: application/json + +{"data": {"_arg1": 1, "_arg2": 2}, "script": "return _arg1+_arg2"} +``` + +Example response: + +```HTTP +HTTP/1.1 200 OK +Content-Type: application/json + +3 +``` + +Using curl: + +```bash +curl -X POST http://localhost:9004/evaluate \ +-d '{"data": {"_arg1": 1, "_arg2": 2}, "script": "return _arg1 + _arg2"}' +``` + +It is possible to call a deployed function from within the code block, through +the predefined function `tabpy.query`. This function works like the client +library's `query` method, and returns the corresponding data structure. The +function must first be deployed as an endpoint in the server (for more details +see the [TabPy Tools documentation](tabpy-tools.md)). + +The following example calls the endpoint `clustering` as it was deployed in the +section [deploy-function](tabpy-tools.md#deploying-a-function): + +```HTTP +POST /evaluate HTTP/1.1 +Host: example.com +Accept: application/json + +{ "data": + { "_arg1": [6.35, 6.40, 6.65, 8.60, 8.90, 9.00, 9.10], + "_arg2": [1.95, 1.95, 2.05, 3.05, 3.05, 3.10, 3.15] + }, + "script": "return tabpy.query('clustering', x=_arg1, y=_arg2)"} +``` + +The next example shows how to call `evaluate` from a terminal using curl; this +code queries the method `add` that was deployed in the section +[deploy-function](tabpy-tools.md#deploying-a-function): + +```bash +curl -X POST http://localhost:9004/evaluate \ +-d '{"data": {"_arg1":1, "_arg2":2}, + "script": "return tabpy.query(\"add\", x=_arg1, y=_arg2)[\"response\"]"}' +``` + +## http:post:: /query/:endpoint + +Executes a function at the specified endpoint. The function must first be +deployed (see the [TabPy Tools documentation](tabpy-tools.md)). + +This interface expects a JSON body with a `data` key, specifying the values +for the function, according to its original definition. In the example below, +the function `clustering` was defined with a signature of two parameters `x` +and `y`, expecting arrays of numbers. + +Example request: + +```HTTP +POST /query/clustering HTTP/1.1 +Host: localhost:9004 +Accept: application/json + +{"data": { + "x": [6.35, 6.40, 6.65, 8.60, 8.90, 9.00, 9.10], + "y": [1.95, 1.95, 2.05, 3.05, 3.05, 3.10, 3.15]}} +``` + +Example response: + +```HTTP +HTTP/1.1 200 OK +Content-Type: application/json + +{"model": "clustering", "version": 1, "response": [0, 0, 0, 1, 1, 1, 1], + "uuid": "46d3df0e-acca-4560-88f1-67c5aedeb1c4"} +``` + +Using curl: + +```bash +curl -X GET http://localhost:9004/query/clustering -d \ +'{"data": {"x": [6.35, 6.40, 6.65, 8.60, 8.90, 9.00, 9.10], + "y": [1.95, 1.95, 2.05, 3.05, 3.05, 3.10, 3.15]}}' +``` diff --git a/docs/server-config.md b/docs/server-config.md index 6d1b9474..b0d573d7 100755 --- a/docs/server-config.md +++ b/docs/server-config.md @@ -8,6 +8,7 @@ - [Authentication](#authentication) * [Enabling Authentication](#enabling-authentication) * [Password File](#password-file) + * [Setting Up Environmnet](#setting-up-environmnet) * [Adding an Account](#adding-an-account) * [Updating an Account](#updating-an-account) * [Deleting an Account](#deleting-an-account) @@ -94,6 +95,21 @@ see how to use it. After making any changes to the password file, TabPy needs to be restarted. +### Setting Up Environmnet + +Before making any code changes run environment setup script. For +Windows run the next command from the repository root folder: + +```sh +utils\set_env.cmd +``` + +and for Linux or Mac the next command from the repository root folder: + +```sh +utils/set_env.sh +``` + ### Adding an Account To add an account run `utils/user_management.py` utility with `add` @@ -145,6 +161,7 @@ URL, client infomation (Tableau Desktop\Server), Tableau user name (for Tableau and TabPy user name as shown in the example below: + ``` 2019-04-17,15:20:37 [INFO] (evaluation_plane_handler.py:evaluation_plane_handler:86): ::1 calls POST http://localhost:9004/evaluate, @@ -157,4 +174,5 @@ function to evaluate=def _user_script(tabpy, _arg1, _arg2): res.append(_arg1[i] * _arg2[i]) return res ``` + diff --git a/docs/server-rest.md b/docs/server-rest.md index 16b92e31..902455d8 100755 --- a/docs/server-rest.md +++ b/docs/server-rest.md @@ -3,282 +3,112 @@ The server process exposes several REST APIs to get status and to execute Python code and query deployed methods. + + -- [http:get:: /info](#httpget-info) -- [http:get:: /status](#httpget-status) -- [http:get:: /endpoints](#httpget-endpoints) -- [http:get:: /endpoints/:endpoint](#httpget-endpointsendpoint) -- [http:post:: /evaluate](#httppost-evaluate) -- [http:post:: /query/:endpoint](#httppost-queryendpoint) +- [GET /info](#get-info) + * [URL](#url) + * [Method](#method) + * [URL parameters](#url-parameters) + * [Data Parameters](#data-parameters) + * [Response](#response) +- [API versions](#api-versions) -## http:get:: /info - -Get static information about the server. - -Example request: - -```HTTP -GET /info HTTP/1.1 -Host: localhost:9004 -Accept: application/json -``` - -Example response: - -```HTTP -HTTP/1.1 200 OK -Content-Type: application/json - -{"description": "", - "creation_time": "0", - "state_path": "/Users/username/my-server-state-folder", - "server_version": "dev", - "name": "my-server-name"} - -``` - -- `description` is a string that is hardcoded in the `state.ini` file and - can be edited there. -- `creation_time` is the creation time in seconds since 1970-01-01, hardcoded - in the `state.ini` file, where it can be edited. -- `state_path` is the state file path of the server (the value of the - TABPY_STATE_PATH at the time the server was started). -- `server_version` is the TabPy Server version tag. Clients can use this - information for compatibility checks. - -See [TabPy Configuration](#tabpy-configuration) section for more information -on modifying the settings. - -Using curl: - -```bash -curl -X GET http://localhost:9004/info -``` + -## http:get:: /status +## GET /info -Gets runtime status of deployed endpoints. If no endpoints are deployed in -the server, the returned data is an empty JSON object. +Get static information about the server. The method doesn't require any +authentication and returns supported API versions client can use together +with optional and required features. -Example request: +### URL ```HTTP -GET /status HTTP/1.1 -Host: localhost:9004 -Accept: application/json +/info ``` -Example response: +### Method ```HTTP -HTTP/1.1 200 OK -Content-Type: application/json - -{"clustering": { - "status": "LoadSuccessful", - "last_error": null, - "version": 1, - "type": "model"}, - "add": { - "status": "LoadSuccessful", - "last_error": null, - "version": 1, - "type": "model"} -} -``` - -Using curl: - -```bash -curl -X GET http://localhost:9004/status +GET ``` -## http:get:: /endpoints - -Gets a list of deployed endpoints and their static information. If no -endpoints are deployed in the server, the returned data is an empty JSON object. - -Example request: - -```HTTP -GET /endpoints HTTP/1.1 -Host: localhost:9004 -Accept: application/json -``` +### URL parameters -Example response: +None. -```HTTP -HTTP/1.1 200 OK -Content-Type: application/json - -{"clustering": - {"description": "", - "docstring": "-- no docstring found in query function --", - "creation_time": 1469511182, - "version": 1, - "dependencies": [], - "last_modified_time": 1469511182, - "type": "model", - "target": null}, -"add": { - "description": "", - "docstring": "-- no docstring found in query function --", - "creation_time": 1469505967, - "version": 1, - "dependencies": [], - "last_modified_time": 1469505967, - "type": "model", - "target": null} -} -``` +### Data Parameters -Using curl: +None. -```bash -curl -X GET http://localhost:9004/endpoints -``` +### Response -## http:get:: /endpoints/:endpoint +For successful call: -Gets the description of a specific deployed endpoint. The endpoint must first -be deployed in the server (see the [TabPy Tools documentation](tabpy-tools.md)). +- Status: 200 +- Content: -Example request: + ```json + { + "description": "", + "creation_time": "0", + "state_path": "e:\\dev\\TabPy\\tabpy-server\\tabpy_server", + "server_version": "0.4.1", + "name": "TabPy Server", + "versions": { + "v1": { + "features": { + "authentication": { + "required": true, + "methods": { + "basic-auth": {} + } + } + } + } + } + } + ``` -```HTTP -GET /endpoints/add HTTP/1.1 -Host: localhost:9004 -Accept: application/json -``` +Response fields: -Example response: + -```HTTP -HTTP/1.1 200 OK -Content-Type: application/json +Property | Description +--- | --- +`description` | String that is hardcoded in the `state.ini` file and can be edited there. +`creation_time` | creation time in seconds since 1970-01-01, hardcoded in the `state.ini` file, where it can be edited. +`state_path` | state file path of the server (the value of the TABPY_STATE_PATH at the time the server was started). +`server_version` | TabPy Server version tag. Clients can use this information for compatibility checks. +`name` | TabPy server instance name. Can be edited in `state.ini` file. +`version` | Collection of API versions supported by the server. Each entry in the collection is an API version which has corresponding list of properties. +`version.`*``* | Set of properties for an API version. +`version.`*`.features`* | Set of an API available features. +`version.`*`.features.`* | Set of a features properties. For specific details for property meaning of a feature check documentation for specific API version. +`version.`*`.features..required`* | If true the feature is required to be used by client. -{"description": "", "docstring": "-- no docstring found in query function --", - "creation_time": 1469505967, "version": 1, "dependencies": [], - "last_modified_time": 1469505967, "type": "model", "target": null} -``` + -Using curl: +For each API version there is set of properties, e.g. for v1 in the example +above features are: -```bash -curl -X GET http://localhost:9004/endpoints/add -``` - -## http:post:: /evaluate - -Executes a block of Python code, replacing named parameters with their provided -values. - -The expected POST body is a JSON dictionary with two elements: - -- A key `data` with a value that contains the parameter values passed to the - code. These values are key-value pairs, following a specific convention for - key names (`_arg1`, `_arg2`, etc.). -- A key `script` with a value that contains the Python code (one or more lines). - Any references to the parameter names will be replaced by their values - according to `data`. - -Example request: - -```HTTP -POST /evaluate HTTP/1.1 -Host: localhost:9004 -Accept: application/json - -{"data": {"_arg1": 1, "_arg2": 2}, "script": "return _arg1+_arg2"} -``` - -Example response: - -```HTTP -HTTP/1.1 200 OK -Content-Type: application/json - -3 -``` - -Using curl: - -```bash -curl -X POST http://localhost:9004/evaluate \ --d '{"data": {"_arg1": 1, "_arg2": 2}, "script": "return _arg1 + _arg2"}' -``` - -It is possible to call a deployed function from within the code block, through -the predefined function `tabpy.query`. This function works like the client -library's `query` method, and returns the corresponding data structure. The -function must first be deployed as an endpoint in the server (for more details -see the [TabPy Tools documentation](tabpy-tools.md)). - -The following example calls the endpoint `clustering` as it was deployed in the -section [deploy-function](tabpy-tools.md#deploying-a-function): +See [TabPy Configuration](#tabpy-configuration) section for more information +on modifying the settings. -```HTTP -POST /evaluate HTTP/1.1 -Host: example.com -Accept: application/json - -{ "data": - { "_arg1": [6.35, 6.40, 6.65, 8.60, 8.90, 9.00, 9.10], - "_arg2": [1.95, 1.95, 2.05, 3.05, 3.05, 3.10, 3.15] - }, - "script": "return tabpy.query('clustering', x=_arg1, y=_arg2)"} -``` +- **Examples** -The next example shows how to call `evaluate` from a terminal using curl; this -code queries the method `add` that was deployed in the section -[deploy-function](tabpy-tools.md#deploying-a-function): +Calling the method with curl: ```bash -curl -X POST http://localhost:9004/evaluate \ --d '{"data": {"_arg1":1, "_arg2":2}, - "script": "return tabpy.query(\"add\", x=_arg1, y=_arg2)[\"response\"]"}' -``` - -## http:post:: /query/:endpoint - -Executes a function at the specified endpoint. The function must first be -deployed (see the [TabPy Tools documentation](tabpy-tools.md)). - -This interface expects a JSON body with a `data` key, specifying the values -for the function, according to its original definition. In the example below, -the function `clustering` was defined with a signature of two parameters `x` -and `y`, expecting arrays of numbers. - -Example request: - -```HTTP -POST /query/clustering HTTP/1.1 -Host: localhost:9004 -Accept: application/json - -{"data": { - "x": [6.35, 6.40, 6.65, 8.60, 8.90, 9.00, 9.10], - "y": [1.95, 1.95, 2.05, 3.05, 3.05, 3.10, 3.15]}} +curl -X GET http://localhost:9004/info ``` -Example response: +## API versions -```HTTP -HTTP/1.1 200 OK -Content-Type: application/json +TabPy supports the following API versions: -{"model": "clustering", "version": 1, "response": [0, 0, 0, 1, 1, 1, 1], - "uuid": "46d3df0e-acca-4560-88f1-67c5aedeb1c4"} -``` - -Using curl: - -```bash -curl -X GET http://localhost:9004/query/clustering -d \ -'{"data": {"x": [6.35, 6.40, 6.65, 8.60, 8.90, 9.00, 9.10], - "y": [1.95, 1.95, 2.05, 3.05, 3.05, 3.10, 3.15]}}' -``` +- v1 - see details at [api-v1.md](api-v1.md). diff --git a/startup.cmd b/startup.cmd index 4aba054c..5472dadd 100755 --- a/startup.cmd +++ b/startup.cmd @@ -9,7 +9,7 @@ SET SAVE_PYTHONPATH=%PYTHONPATH% ECHO Checking for presence of Python in the system path variable. -python --version >nul 2>&1 +python --version IF %ERRORLEVEL% NEQ 0 ( ECHO Cannot find Python.exe. Check that Python is installed and is in the system PATH environment variable. SET RET=1 diff --git a/startup.sh b/startup.sh index 04633074..c511ac62 100755 --- a/startup.sh +++ b/startup.sh @@ -27,7 +27,7 @@ function install_dependencies() { # Check for Python in PATH echo Checking for presence of Python in the system path variable. -python --version &>- +python3 --version check_status "Cannot find Python. Check that Python is installed and is in the system PATH environment variable." # Setting local variables diff --git a/tabpy-server/tabpy_server/app/app.py b/tabpy-server/tabpy_server/app/app.py index fe6e4122..96a48f12 100644 --- a/tabpy-server/tabpy_server/app/app.py +++ b/tabpy-server/tabpy_server/app/app.py @@ -232,7 +232,7 @@ def set_parameter(settings_key, self.settings[SettingsParameters.StaticPath] =\ os.path.abspath(self.settings[SettingsParameters.StaticPath]) logger.debug(f'Static pages folder set to ' - '"{self.settings[SettingsParameters.StaticPath]}"') + f'"{self.settings[SettingsParameters.StaticPath]}"') # Set subdirectory from config if applicable if tabpy_state.has_option("Service Info", "Subdirectory"): diff --git a/tabpy-server/tabpy_server/handlers/base_handler.py b/tabpy-server/tabpy_server/handlers/base_handler.py index 7ea8bc4d..316bd5f5 100644 --- a/tabpy-server/tabpy_server/handlers/base_handler.py +++ b/tabpy-server/tabpy_server/handlers/base_handler.py @@ -316,7 +316,7 @@ def append_request_context(self, msg) -> str: if self.log_request_context: # log request details context = (f'{self.request.remote_ip} calls ' - '{self.request.method} {self.request.full_url()}') + f'{self.request.method} {self.request.full_url()}') if 'TabPy-Client' in self.request.headers: context += f', Client: {self.request.headers["TabPy-Client"]}' if 'TabPy-User' in self.request.headers: diff --git a/tests/integration/integ_test_base.py b/tests/integration/integ_test_base.py new file mode 100755 index 00000000..220b56dc --- /dev/null +++ b/tests/integration/integ_test_base.py @@ -0,0 +1,251 @@ +import http.client +import os +import platform +import shutil +import signal +import subprocess +import tempfile +import time +import unittest + + +class IntegTestBase(unittest.TestCase): + ''' + Base class for integration tests. + ''' + def __init__(self, methodName="runTest"): + super(IntegTestBase, self).__init__(methodName) + self.process = None + self.delete_temp_folder = True + + def set_delete_temp_folder(self, delete_temp_folder: bool): + ''' + Specify if temporary folder for state, config and log + files should be deleted when test is done. + By default the folder is deleted. + + Parameters + ---------- + delete_test_folder: bool + If True temp folder will be deleted. + ''' + self.delete_temp_folder = delete_temp_folder + + def _get_state_file_path(self) -> str: + ''' + Generates state.ini and returns absolute path to it. + Overwrite this function for tests to run against not default state + file. + + Returns + ------- + str + Absolute path to state file folder. + ''' + state_file = open(os.path.join(self.tmp_dir, 'state.ini'), 'w+') + state_file.write( + '[Service Info]\n' + 'Name = TabPy Serve\n' + 'Description = \n' + 'Creation Time = 0\n' + 'Access-Control-Allow-Origin = \n' + 'Access-Control-Allow-Headers = \n' + 'Access-Control-Allow-Methods = \n' + '\n' + '[Query Objects Service Versions]\n' + '\n' + '[Query Objects Docstrings]\n' + '\n' + '[Meta]\n' + 'Revision Number = 1\n') + state_file.close() + + return self.tmp_dir + + def _get_port(self) -> str: + ''' + Returns port TabPy should run on. Default implementation + returns '9004'. + + Returns + ------- + str + Port number. + ''' + return '9004' + + def _get_pwd_file(self) -> str: + ''' + Returns absolute or relative path to password file. + Overwrite to create and/or specify your own file. + Default implementation returns None which means + TABPY_PWD_FILE setting won't be added to config. + + Returns + ------- + str + Absolute or relative path to password file. + If None TABPY_PWD_FILE setting won't be added to + config. + ''' + return None + + def _get_transfer_protocol(self) -> str: + ''' + Returns transfer protocol for configuration file. + Default implementation returns None which means + TABPY_TRANSFER_PROTOCOL setting won't be added to config. + + Returns + ------- + str + Transfer protocol (e.g 'http' or 'https'). + If None TABPY_TRANSFER_PROTOCOL setting won't be + added to config. + ''' + return None + + def _get_certificate_file_name(self) -> str: + ''' + Returns absolute or relative certificate file name + for configuration file. + Default implementation returns None which means + TABPY_CERTIFICATE_FILE setting won't be added to config. + + Returns + ------- + str + Absolute or relative certificate file name. + If None TABPY_CERTIFICATE_FILE setting won't be + added to config. + ''' + return None + + def _get_key_file_name(self) -> str: + ''' + Returns absolute or relative private key file name + for configuration file. + Default implementation returns None which means + TABPY_KEY_FILE setting won't be added to config. + + Returns + ------- + str + Absolute or relative private key file name. + If None TABPY_KEY_FILE setting won't be + added to config. + ''' + return None + + def _get_config_file_name(self) -> str: + ''' + Generates config file. Overwrite this function for tests to + run against not default state file. + + Returns + ------- + str + Absolute path to config file. + ''' + config_file = open(os.path.join(self.tmp_dir, 'test.conf'), 'w+') + config_file.write( + '[TabPy]\n' + f'TABPY_PORT = {self._get_port()}\n' + f'TABPY_STATE_PATH = {self.tmp_dir}\n') + + pwd_file = self._get_pwd_file() + if pwd_file is not None: + pwd_file = os.path.abspath(pwd_file) + config_file.write(f'TABPY_PWD_FILE = {pwd_file}\n') + + transfer_protocol = self._get_transfer_protocol() + if transfer_protocol is not None: + config_file.write( + f'TABPY_TRANSFER_PROTOCOL = {transfer_protocol}\n') + + cert_file_name = self._get_certificate_file_name() + if cert_file_name is not None: + cert_file_name = os.path.abspath(cert_file_name) + config_file.write(f'TABPY_CERTIFICATE_FILE = {cert_file_name}\n') + + key_file_name = self._get_key_file_name() + if key_file_name is not None: + key_file_name = os.path.abspath(key_file_name) + config_file.write(f'TABPY_KEY_FILE = {key_file_name}\n') + + config_file.close() + + self.delete_config_file = True + return config_file.name + + def setUp(self): + super(IntegTestBase, self).setUp() + prefix = 'TabPy_IntegTest_' + self.tmp_dir = tempfile.mkdtemp(prefix=prefix) + + # create temporary state.ini + orig_state_file_name = os.path.abspath( + self._get_state_file_path() + '/state.ini') + self.state_file_name = os.path.abspath(self.tmp_dir + '/state.ini') + if orig_state_file_name != self.state_file_name: + shutil.copyfile(orig_state_file_name, self.state_file_name) + + # create config file + orig_config_file_name = os.path.abspath(self._get_config_file_name()) + self.config_file_name = os.path.abspath( + self.tmp_dir + '/' + + os.path.basename(orig_config_file_name)) + if orig_config_file_name != self.config_file_name: + shutil.copyfile(orig_config_file_name, self.config_file_name) + + # Platform specific - for integration tests we want to engage + # startup script + with open(self.tmp_dir + '/output.txt', 'w') as outfile: + if platform.system() == 'Windows': + self.process = subprocess.Popen( + ['startup.cmd', self.config_file_name], + stdout=outfile, + stderr=outfile) + else: + self.process = subprocess.Popen( + ['./startup.sh', + '--config=' + self.config_file_name], + preexec_fn=os.setsid, + stdout=outfile, + stderr=outfile) + + # give the app some time to start up... + time.sleep(5) + + def tearDown(self): + # stop TabPy + if self.process is not None: + if platform.system() == 'Windows': + subprocess.call(['taskkill', '/F', '/T', '/PID', + str(self.process.pid)]) + else: + os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) + self.process.kill() + + # after shutting down TabPy and before we start it again + # for next test give it some time to terminate. + time.sleep(5) + + # remove temporary files + if self.delete_temp_folder: + os.remove(self.state_file_name) + os.remove(self.config_file_name) + shutil.rmtree(self.tmp_dir) + + super(IntegTestBase, self).tearDown() + + def _get_connection(self) -> http.client.HTTPConnection: + protocol = self._get_transfer_protocol() + url = 'localhost:' + self._get_port() + + if protocol is not None and protocol.lower() == 'https': + connection = http.client.HTTPSConnection(url) + else: + connection = http.client.HTTPConnection(url) + + return connection diff --git a/tests/integration/resources/2019_04_24_to_3018_08_25.crt b/tests/integration/resources/2019_04_24_to_3018_08_25.crt new file mode 100644 index 00000000..69bf52ee --- /dev/null +++ b/tests/integration/resources/2019_04_24_to_3018_08_25.crt @@ -0,0 +1,23 @@ +-----BEGIN CERTIFICATE----- +MIIDyjCCArICCQD3+Tk53RbuzTANBgkqhkiG9w0BAQsFADCBpTELMAkGA1UEBhMC +VVMxCzAJBgNVBAgMAldBMREwDwYDVQQHDAhLaXJrbGFuZDEZMBcGA1UECgwQVGFi +bGVhdSBTb2Z0d2FyZTEbMBkGA1UECwwSQWR2YW5jZWQgQW5hbHl0aWNzMRcwFQYD +VQQDDA5PbGVrIEdvbG92YXR5aTElMCMGCSqGSIb3DQEJARYWb2dvbG92YXR5aUB0 +YWJsZWF1LmNvbTAgFw0xOTA0MjQyMTU1NDVaGA8zMDE4MDgyNTIxNTU0NVowgaUx +CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJXQTERMA8GA1UEBwwIS2lya2xhbmQxGTAX +BgNVBAoMEFRhYmxlYXUgU29mdHdhcmUxGzAZBgNVBAsMEkFkdmFuY2VkIEFuYWx5 +dGljczEXMBUGA1UEAwwOT2xlayBHb2xvdmF0eWkxJTAjBgkqhkiG9w0BCQEWFm9n +b2xvdmF0eWlAdGFibGVhdS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK +AoIBAQCnoOgC0w3mSS2uoRQOcKtkC3ueHxo8hDXsdnBaCdcvo8ixqvYiKP/twZCb +sz+5YGFGkCwGWrdX9U9Iy/70r1fLyoZ89oswjf4ei3FczFfjTB1l4pgnDBYKWgQm +IdkZ3n26YmNWm/4e3cm61KYY8fJN0v9Ql5NBxH+xRrvwqgkFRZJcIuAEa7k28FD/ +KaMLOgDMxtuFXcoQSwT75ggmhM89aeE4kKf+MbG7dkwoV3y1hZG/gW6BryLfo2xA +YlaQwtzPBPhgE8gsqxtO7l+wxv03JOnkPQNBWHAf7MtlkqdM6g03UrWmfTFhqzPE +rzsiWOXDxD2c5HSiss24HHrgF37rAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAIQb +TwPcrDeYUwIz8TWEEcIX3jJoDMyR6Q+AEmM8C8ed0JlavE2qPiZc+lLr8Dc1B+fE +7UkxHPZOw0fCtrQ3I3+0Z/6YfqZs3m1f1I8Yr6SSW6NjAj7+mmMQ8DuJGb5yuefP +Z0LV8F+OwUXNI7bsl0Q4UKt8QQ0ovI3I6w8HVsuy7zyEUN268tiK58bMkSfbzVal +UvIcXqZyBFKQ/ZZ3BknI8b3ibya7h7R/92CsMDfPAQASZcBwKJ64RW9Wi5Gzzqmp +D2Vk6MdyOgp4bD9wDqm4f6p20FewagSL5/c3lk1EjoCye6UAH2cnqPRTowI2elJg +W9mrYH2k9L2cnnUIyx4= +-----END CERTIFICATE----- diff --git a/tests/integration/resources/2019_04_24_to_3018_08_25.key b/tests/integration/resources/2019_04_24_to_3018_08_25.key new file mode 100644 index 00000000..bd8c448e --- /dev/null +++ b/tests/integration/resources/2019_04_24_to_3018_08_25.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAp6DoAtMN5kktrqEUDnCrZAt7nh8aPIQ17HZwWgnXL6PIsar2 +Iij/7cGQm7M/uWBhRpAsBlq3V/VPSMv+9K9Xy8qGfPaLMI3+HotxXMxX40wdZeKY +JwwWCloEJiHZGd59umJjVpv+Ht3JutSmGPHyTdL/UJeTQcR/sUa78KoJBUWSXCLg +BGu5NvBQ/ymjCzoAzMbbhV3KEEsE++YIJoTPPWnhOJCn/jGxu3ZMKFd8tYWRv4Fu +ga8i36NsQGJWkMLczwT4YBPILKsbTu5fsMb9NyTp5D0DQVhwH+zLZZKnTOoNN1K1 +pn0xYaszxK87Iljlw8Q9nOR0orLNuBx64Bd+6wIDAQABAoIBACsVRAxVymDBtigH +5mu/sY1JFkCRpeCf6mwYFNBPbysjYVWopxIoj37AHTanX1151Aaaz3XiovTMa9A9 +/g1Nc7dBGkfL5gJYvFOFa2F6c6xLx9KD5q9Cf/exIxfZ4z6u3Imm9/kupqWwQ0Tt +mrMWnDw8WrqP+p0Qr/EUSQGV8jOUP3SyA3zQbEbsBkettz4Nil9NITwbQrbnG6o7 +RnNuxCwRwO3QB1p0YOXnKytU8xIu78Xg3qwVx81QP3/omB9jNCY52p1FRLzGO21q +JFZLIcP7hECesn5v9dZa99oChU+Rdzi93VEymwmZBVl8givNdWAe1Y7c/Z8fIWS/ +FQVvRgECgYEA12uZRlLDnDoqYTj5Ots87Kpy15wU5lA2ASGNgaQLXZBgh8ID/8o5 +QRk1/CNW2i/cW/fI7XRaiFoHjrvD6XgT0m/bWOezFWXoemkMR/jR2kYZjS0vpc04 +wd/rvrHr4nbSQE1cVBwLrYzrYJ6qrGybx9P6k0fhu2fUIJIJGEOTQ0ECgYEAxzSa +f4KLGSrZjfZl695z+83VUG5aeg8V407nA6RBw3XeK9BjHhqfsRnFAfvhtyTolm3M +QOaZLhSnhnW5HSdYW0QEtW4Lb5GkiGdZSArjM5zD/MgHitlOm9r0IL+nBbtNQYrd +i85pTeeIlG7CTFGtx3b9EiQYHYl2xeS3QbblcysCgYBvQVPk3OPHsMaodZtKSWY6 +uIEdV6/3jt+FUAXcOZPhG6qvEoWsOo29UD7wXHQDtYoyOVOdR2VmXFDg55pz3p8m +JLz9OpTj7UDWz6AXH6uJ9oBFyFt+XvH8NyBy2UMBL+rAaPPRQLbLSCdcPDXbXTBL +UPBt1kb/2czVkXZ/AI9ywQKBgQCGgsm0QhTk6J9AkdmenHZa2FEq32kutFMGSzgI +qHhToJploW/cWwPr1UfHICr4vO5k7T0Xsd5LVF0OmR1nRzMNZW98hxMnwgOEq6yI +zfk+16MrZHJbWoMPEJj6KA+C+kefc0JH7hgDJ8181RFT8W9TmdAm2MKD51eRJvBr +ajGjQwKBgQCvNf8Ds4Smy/5ANyOjK3/iPZiGiVgyaKCJOKNHQ+pAD0JS8XfOh/Km +KiXv8jBEQcChB7YoYKBUfXwpSLFruJU3kCLvN4MHQAgV0BfVx8MkcLJh6K+wMGPX +Es5hj6r4RQQblJaj9q8qb3+9uG3k7Sn4TXc0TYg7ml32ugXSXMxfKg== +-----END RSA PRIVATE KEY----- diff --git a/tests/integration/resources/pwdfile.txt b/tests/integration/resources/pwdfile.txt new file mode 100755 index 00000000..1af7af4b --- /dev/null +++ b/tests/integration/resources/pwdfile.txt @@ -0,0 +1 @@ +user1 b8a63cf588cd2399da615042de4732f5b121c4d1042acfb91598a56d2dd3e1d7b9f785213262eddbbd00c7a8c9c3e89d7cb98f31d405cc644b1aeba92c3de40b diff --git a/tests/integration/test_auth.py b/tests/integration/test_auth.py new file mode 100755 index 00000000..c730d8ba --- /dev/null +++ b/tests/integration/test_auth.py @@ -0,0 +1,75 @@ +import base64 +import integ_test_base + + +class TestAuth(integ_test_base.IntegTestBase): + def setUp(self): + super(TestAuth, self).setUp() + self.payload = ( + '''{ + "data": { "_arg1": [1, 2] }, + "script": "return [x * 2 for x in _arg1]" + }''') + + def _get_pwd_file(self) -> str: + return './tests/integration/resources/pwdfile.txt' + + def test_missing_credentials_fails(self): + headers = { + 'Content-Type': "application/json", + 'TabPy-Client': "Integration tests for Auth" + } + + conn = self._get_connection() + conn.request("POST", "/evaluate", self.payload, headers) + res = conn.getresponse() + + self.assertEqual(401, res.status) + + def test_invalid_password(self): + headers = { + 'Content-Type': "application/json", + 'TabPy-Client': "Integration tests for Auth", + 'Authorization': + 'Basic ' + + base64.b64encode('user1:wrong_password'.encode('utf-8')). + decode('utf-8') + } + + conn = self._get_connection() + conn.request("POST", "/evaluate", self.payload, headers) + res = conn.getresponse() + + self.assertEqual(401, res.status) + + def test_invalid_username(self): + headers = { + 'Content-Type': "application/json", + 'TabPy-Client': "Integration tests for Auth", + 'Authorization': + 'Basic ' + + base64.b64encode('wrong_user:P@ssw0rd'.encode('utf-8')). + decode('utf-8') + } + + conn = self._get_connection() + conn.request("POST", "/evaluate", self.payload, headers) + res = conn.getresponse() + + self.assertEqual(401, res.status) + + def test_valid_credentials(self): + headers = { + 'Content-Type': "application/json", + 'TabPy-Client': "Integration tests for Auth", + 'Authorization': + 'Basic ' + + base64.b64encode('user1:P@ssw0rd'.encode('utf-8')). + decode('utf-8') + } + + conn = self._get_connection() + conn.request("POST", "/evaluate", self.payload, headers) + res = conn.getresponse() + + self.assertEqual(200, res.status) diff --git a/tests/integration/test_url.py b/tests/integration/test_url.py new file mode 100755 index 00000000..74785d88 --- /dev/null +++ b/tests/integration/test_url.py @@ -0,0 +1,14 @@ +''' +All other misc. URL-related integration tests. +''' + +import integ_test_base + + +class TestURL(integ_test_base.IntegTestBase): + def test_notexistent_url(self): + conn = self._get_connection() + conn.request("GET", "/unicorn") + res = conn.getresponse() + + self.assertEqual(404, res.status) diff --git a/tests/integration/test_url_ssl.py b/tests/integration/test_url_ssl.py new file mode 100755 index 00000000..0f300b46 --- /dev/null +++ b/tests/integration/test_url_ssl.py @@ -0,0 +1,32 @@ +''' +All other misc. URL-related integration tests for +when SSL is turned on for TabPy. +''' + +import integ_test_base +import requests + + +class TestURL_SSL(integ_test_base.IntegTestBase): + def _get_port(self): + return '9005' + + def _get_transfer_protocol(self) -> str: + return 'https' + + def _get_certificate_file_name(self) -> str: + return './tests/integration/resources/2019_04_24_to_3018_08_25.crt' + + def _get_key_file_name(self) -> str: + return './tests/integration/resources/2019_04_24_to_3018_08_25.key' + + def test_notexistent_url(self): + session = requests.Session() + # Do not verify servers' cert to be signed by trusted CA + session.verify = False + # Do not warn about insecure request + requests.packages.urllib3.disable_warnings() + response = session.get( + url=f'https://localhost:{self._get_port()}/unicorn') + + self.assertEqual(404, response.status_code) diff --git a/tests/runtests.py b/tests/runtests.py deleted file mode 100755 index c0da5329..00000000 --- a/tests/runtests.py +++ /dev/null @@ -1,21 +0,0 @@ -import os -import sys -import unittest - - -if __name__ == '__main__': - dirs = {'tabpy-tools', 'tabpy-server'} - - for dir_ in dirs: - sys.path.insert(0, os.path.join( - os.path.abspath(os.path.dirname(__file__)), dir_)) - - # Get all of the tests we need from the two project - suite_list = [] - for dir_ in dirs: - suite_list.append(unittest.TestLoader().discover(dir_)) - - suite = unittest.TestSuite(suite_list) - - runner = unittest.TextTestRunner() - runner.run(suite) diff --git a/tabpy-server/server_tests/__init__.py b/tests/unit/server_tests/__init__.py similarity index 100% rename from tabpy-server/server_tests/__init__.py rename to tests/unit/server_tests/__init__.py diff --git a/tabpy-server/server_tests/resources/expired.crt b/tests/unit/server_tests/resources/expired.crt similarity index 100% rename from tabpy-server/server_tests/resources/expired.crt rename to tests/unit/server_tests/resources/expired.crt diff --git a/tabpy-server/server_tests/resources/future.crt b/tests/unit/server_tests/resources/future.crt similarity index 100% rename from tabpy-server/server_tests/resources/future.crt rename to tests/unit/server_tests/resources/future.crt diff --git a/tabpy-server/server_tests/resources/valid.crt b/tests/unit/server_tests/resources/valid.crt similarity index 100% rename from tabpy-server/server_tests/resources/valid.crt rename to tests/unit/server_tests/resources/valid.crt diff --git a/tabpy-server/server_tests/test_config.py b/tests/unit/server_tests/test_config.py similarity index 100% rename from tabpy-server/server_tests/test_config.py rename to tests/unit/server_tests/test_config.py diff --git a/tabpy-server/server_tests/test_endpoint_handler.py b/tests/unit/server_tests/test_endpoint_handler.py similarity index 95% rename from tabpy-server/server_tests/test_endpoint_handler.py rename to tests/unit/server_tests/test_endpoint_handler.py index 6e276d29..f46a2e03 100755 --- a/tabpy-server/server_tests/test_endpoint_handler.py +++ b/tests/unit/server_tests/test_endpoint_handler.py @@ -1,11 +1,9 @@ import base64 import os import tempfile -import unittest from argparse import Namespace from tabpy_server.app.app import TabPyApp -from tabpy_server.handlers.endpoint_handler import EndpointHandler from tabpy_server.handlers.util import hash_password from tornado.testing import AsyncHTTPTestCase from unittest.mock import patch diff --git a/tabpy-server/server_tests/test_endpoints_handler.py b/tests/unit/server_tests/test_endpoints_handler.py similarity index 100% rename from tabpy-server/server_tests/test_endpoints_handler.py rename to tests/unit/server_tests/test_endpoints_handler.py diff --git a/tabpy-server/server_tests/test_evaluation_plane_handler.py b/tests/unit/server_tests/test_evaluation_plane_handler.py similarity index 100% rename from tabpy-server/server_tests/test_evaluation_plane_handler.py rename to tests/unit/server_tests/test_evaluation_plane_handler.py diff --git a/tabpy-server/server_tests/test_pwd_file.py b/tests/unit/server_tests/test_pwd_file.py similarity index 93% rename from tabpy-server/server_tests/test_pwd_file.py rename to tests/unit/server_tests/test_pwd_file.py index c431cae8..e7132d4f 100755 --- a/tabpy-server/server_tests/test_pwd_file.py +++ b/tests/unit/server_tests/test_pwd_file.py @@ -1,14 +1,8 @@ -import logging -import pathlib import os import unittest -from argparse import Namespace from tempfile import NamedTemporaryFile from tabpy_server.app.app import TabPyApp -from tabpy_server.app.ConfigParameters import ConfigParameters - -from unittest.mock import patch, call class TestPasswordFile(unittest.TestCase): @@ -93,7 +87,7 @@ def test_given_username_but_no_password_expect_parsing_fails(self): "{} {}".format(login, pwd)) with self.assertRaises(RuntimeError) as cm: - app = TabPyApp(self.config_file.name) + TabPyApp(self.config_file.name) ex = cm.exception self.assertEqual('Failed to read password file {}'.format( self.pwd_file.name), ex.args[0]) @@ -111,7 +105,7 @@ def test_given_duplicate_usernames_expect_parsing_fails(self): "{} {}\n{} {}".format(login, pwd, login, pwd)) with self.assertRaises(RuntimeError) as cm: - app = TabPyApp(self.config_file.name) + TabPyApp(self.config_file.name) ex = cm.exception self.assertEqual('Failed to read password file {}'.format( self.pwd_file.name), ex.args[0]) diff --git a/tabpy-server/server_tests/test_service_info_handler.py b/tests/unit/server_tests/test_service_info_handler.py similarity index 100% rename from tabpy-server/server_tests/test_service_info_handler.py rename to tests/unit/server_tests/test_service_info_handler.py diff --git a/tabpy-tools/tools_tests/__init__.py b/tests/unit/tools_tests/__init__.py similarity index 100% rename from tabpy-tools/tools_tests/__init__.py rename to tests/unit/tools_tests/__init__.py diff --git a/tabpy-tools/tools_tests/test_client.py b/tests/unit/tools_tests/test_client.py similarity index 100% rename from tabpy-tools/tools_tests/test_client.py rename to tests/unit/tools_tests/test_client.py diff --git a/tabpy-tools/tools_tests/test_rest.py b/tests/unit/tools_tests/test_rest.py similarity index 100% rename from tabpy-tools/tools_tests/test_rest.py rename to tests/unit/tools_tests/test_rest.py diff --git a/tabpy-tools/tools_tests/test_rest_object.py b/tests/unit/tools_tests/test_rest_object.py similarity index 100% rename from tabpy-tools/tools_tests/test_rest_object.py rename to tests/unit/tools_tests/test_rest_object.py diff --git a/utils/set_env.cmd b/utils/set_env.cmd new file mode 100755 index 00000000..6a7033f0 --- /dev/null +++ b/utils/set_env.cmd @@ -0,0 +1 @@ +set PYTHONPATH=%PYTHONPATH%;./tabpy-server;./tabpy-tools \ No newline at end of file diff --git a/utils/set_env.sh b/utils/set_env.sh new file mode 100755 index 00000000..4f683fcf --- /dev/null +++ b/utils/set_env.sh @@ -0,0 +1 @@ +export PYTHONPATH=./tabpy-server:./tabpy-tools:$PYTHONPATH \ No newline at end of file diff --git a/utils/user_management.py b/utils/user_management.py index 5e3c28e0..52315d09 100755 --- a/utils/user_management.py +++ b/utils/user_management.py @@ -155,11 +155,4 @@ def main(): if __name__ == '__main__': logging.basicConfig(level=logging.DEBUG, format="%(message)s") - # add tabpy-tools and tabpy-server folders to - # PYTHONPATH so code from there can be found when - # modules are imported - for dir_ in {'tabpy-tools', 'tabpy-server'}: - sys.path.insert(0, os.path.join( - os.path.abspath(os.path.dirname(__file__)), dir_)) - main()