Skip to content

Commit 6668045

Browse files
author
lriggs
authored
Merge pull request #548 from tableau/gzipTest
Gzip Support
2 parents 39efa0f + e439225 commit 6668045

File tree

10 files changed

+165
-4
lines changed

10 files changed

+165
-4
lines changed

CHANGELOG

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
# Changelog
22

3+
## v2.5.1
4+
5+
### Improvements
6+
7+
- Gzip encoded requests are now supported by default. This can be disabled in
8+
the config file.
9+
- The INFO method will return the enabled status of features.
10+
311
## v2.5.0
412

513
### Improvements

docs/server-config.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* [Configuration File Content](#configuration-file-content)
99
* [Configuration File Example](#configuration-file-example)
1010
- [Configuring HTTP vs HTTPS](#configuring-http-vs-https)
11+
- [Configuring TPS](#configuring-http-vs-https)
1112
- [Authentication](#authentication)
1213
* [Enabling Authentication](#enabling-authentication)
1314
* [Password File](#password-file)
@@ -93,6 +94,7 @@ at [`logging.config` documentation page](https://docs.python.org/3.6/library/log
9394
value - `30`. This timeout does not apply when evaluating models either
9495
through the `/query` method, or using the `tabpy.query(...)` syntax with
9596
the `/evaluate` method.
97+
- `TABPY_GZIP_ENABLE` - Enable Gzip support for requests. Enabled by default.
9698

9799
### Configuration File Example
98100

tabpy/VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.5.0
1+
2.5.1

tabpy/tabpy_server/app/app.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,15 @@ def run(self):
9696
logger.critical(msg)
9797
raise RuntimeError(msg)
9898

99+
settings = {}
100+
if self.settings[SettingsParameters.GzipEnabled] is True:
101+
settings["decompress_request"] = True
99102
application.listen(
100103
self.settings[SettingsParameters.Port],
101104
ssl_options=ssl_options,
102105
max_buffer_size=max_request_size,
103106
max_body_size=max_request_size,
107+
**settings,
104108
)
105109

106110
logger.info(
@@ -281,6 +285,8 @@ def _parse_config(self, config_file):
281285
"false", None),
282286
(SettingsParameters.MaxRequestSizeInMb, ConfigParameters.TABPY_MAX_REQUEST_SIZE_MB,
283287
100, None),
288+
(SettingsParameters.GzipEnabled, ConfigParameters.TABPY_GZIP_ENABLE,
289+
True, parser.getboolean),
284290
]
285291

286292
for setting, parameter, default_val, parse_function in settings_parameters:
@@ -417,6 +423,7 @@ def _get_features(self):
417423
}
418424

419425
features["evaluate_enabled"] = self.settings[SettingsParameters.EvaluateEnabled]
426+
features["gzip_enabled"] = self.settings[SettingsParameters.GzipEnabled]
420427
return features
421428

422429
def _build_tabpy_state(self):

tabpy/tabpy_server/app/app_parameters.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class ConfigParameters:
1616
TABPY_MAX_REQUEST_SIZE_MB = "TABPY_MAX_REQUEST_SIZE_MB"
1717
TABPY_EVALUATE_ENABLE = "TABPY_EVALUATE_ENABLE"
1818
TABPY_EVALUATE_TIMEOUT = "TABPY_EVALUATE_TIMEOUT"
19+
TABPY_GZIP_ENABLE = "TABPY_GZIP_ENABLE"
1920

2021

2122
class SettingsParameters:
@@ -36,3 +37,4 @@ class SettingsParameters:
3637
MaxRequestSizeInMb = "max_request_size_in_mb"
3738
EvaluateTimeout = "evaluate_timeout"
3839
EvaluateEnabled = "evaluate_enabled"
40+
GzipEnabled = "gzip_enabled"

tabpy/tabpy_server/common/default.conf

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
# The value should be a float representing the timeout time in seconds.
3535
# TABPY_EVALUATE_TIMEOUT = 30
3636

37+
# Enable Gzip compression for requests and responses.
38+
# TABPY_GZIP_ENABLE = true
39+
3740
[loggers]
3841
keys=root
3942

tests/integration/integ_test_base.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,16 @@ def tearDown(self):
263263

264264
super(IntegTestBase, self).tearDown()
265265

266+
def _get_url(self) -> str:
267+
protocol = self._get_transfer_protocol()
268+
url = ""
269+
if protocol is not None and protocol.lower() == "https":
270+
url = "https://"
271+
else:
272+
url = "http://"
273+
url += "localhost:" + self._get_port()
274+
return url
275+
266276
def _get_connection(self) -> http.client.HTTPConnection:
267277
protocol = self._get_transfer_protocol()
268278
url = "localhost:" + self._get_port()

tests/integration/test_gzip.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
"""
2+
Script evaluation tests.
3+
"""
4+
5+
from . import integ_test_base
6+
import json
7+
import gzip
8+
import os
9+
import requests
10+
11+
12+
class TestEvaluate(integ_test_base.IntegTestBase):
13+
def _get_config_file_name(self) -> str:
14+
"""
15+
Generates config file. Overwrite this function for tests to
16+
run against not default state file.
17+
18+
Returns
19+
-------
20+
str
21+
Absolute path to config file.
22+
"""
23+
config_file = open(os.path.join(self.tmp_dir, "test.conf"), "w+")
24+
config_file.write(
25+
"[TabPy]\n"
26+
f"TABPY_QUERY_OBJECT_PATH = {self.tmp_dir}/query_objects\n"
27+
f"TABPY_PORT = {self._get_port()}\n"
28+
f"TABPY_GZIP_ENABLE = TRUE\n"
29+
f"TABPY_STATE_PATH = {self.tmp_dir}\n"
30+
)
31+
32+
pwd_file = self._get_pwd_file()
33+
if pwd_file is not None:
34+
pwd_file = os.path.abspath(pwd_file)
35+
config_file.write(f"TABPY_PWD_FILE = {pwd_file}\n")
36+
37+
transfer_protocol = self._get_transfer_protocol()
38+
if transfer_protocol is not None:
39+
config_file.write(f"TABPY_TRANSFER_PROTOCOL = {transfer_protocol}\n")
40+
41+
cert_file_name = self._get_certificate_file_name()
42+
if cert_file_name is not None:
43+
cert_file_name = os.path.abspath(cert_file_name)
44+
config_file.write(f"TABPY_CERTIFICATE_FILE = {cert_file_name}\n")
45+
46+
key_file_name = self._get_key_file_name()
47+
if key_file_name is not None:
48+
key_file_name = os.path.abspath(key_file_name)
49+
config_file.write(f"TABPY_KEY_FILE = {key_file_name}\n")
50+
51+
evaluate_timeout = self._get_evaluate_timeout()
52+
if evaluate_timeout is not None:
53+
config_file.write(f"TABPY_EVALUATE_TIMEOUT = {evaluate_timeout}\n")
54+
55+
config_file.close()
56+
57+
self.delete_config_file = True
58+
return config_file.name
59+
60+
def test_single_value_returned(self):
61+
payload = """
62+
{
63+
"data": { "_arg1": 2, "_arg2": 40 },
64+
"script":
65+
"return _arg1 + _arg2"
66+
}
67+
"""
68+
headers = {
69+
"Content-Type": "application/json",
70+
"Content-Encoding": "gzip",
71+
}
72+
73+
url = self._get_url() + "/evaluate"
74+
response = requests.request("POST", url, data=gzip.compress(payload.encode('utf-8')),
75+
headers=headers)
76+
result = json.loads(response.text)
77+
78+
self.assertEqual(200, response.status_code)
79+
self.assertEqual(42, result)
80+
81+
def test_syntax_error(self):
82+
payload = """
83+
{
84+
"data": { "_arg1": [2], "_arg2": [40] },
85+
"script":
86+
"% ^ !! return Nothing"
87+
}
88+
"""
89+
headers = {
90+
"Content-Type": "application/json",
91+
"Content-Encoding": "gzip",
92+
}
93+
94+
url = self._get_url() + "/evaluate"
95+
response = requests.request("POST", url, data=gzip.compress(payload.encode('utf-8')),
96+
headers=headers)
97+
result = json.loads(response.text)
98+
99+
self.assertEqual(500, response.status_code)
100+
self.assertEqual("Error processing script", result["message"])
101+
self.assertTrue(result["info"].startswith("SyntaxError"))

tests/unit/server_tests/test_config.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,33 @@ def test_env_variables_in_config(
161161
app = TabPyApp(self.config_file.name)
162162
self.assertEqual(app.settings["port"], "bazbar")
163163

164+
@patch("tabpy.tabpy_server.app.app.os.path.exists", return_value=True)
165+
@patch("tabpy.tabpy_server.app.app._get_state_from_file")
166+
@patch("tabpy.tabpy_server.app.app.TabPyState")
167+
def test_gzip_setting_on_valid(
168+
self, mock_state, mock_get_state_from_file, mock_path_exists
169+
):
170+
self.assertTrue(self.config_file is not None)
171+
config_file = self.config_file
172+
config_file.write("[TabPy]\n" "TABPY_GZIP_ENABLE = true".encode())
173+
config_file.close()
174+
175+
app = TabPyApp(self.config_file.name)
176+
self.assertEqual(app.settings["gzip_enabled"], True)
177+
178+
@patch("tabpy.tabpy_server.app.app.os.path.exists", return_value=True)
179+
@patch("tabpy.tabpy_server.app.app._get_state_from_file")
180+
@patch("tabpy.tabpy_server.app.app.TabPyState")
181+
def test_gzip_setting_off_valid(
182+
self, mock_state, mock_get_state_from_file, mock_path_exists
183+
):
184+
self.assertTrue(self.config_file is not None)
185+
config_file = self.config_file
186+
config_file.write("[TabPy]\n" "TABPY_GZIP_ENABLE = false".encode())
187+
config_file.close()
188+
189+
app = TabPyApp(self.config_file.name)
190+
self.assertEqual(app.settings["gzip_enabled"], False)
164191

165192
class TestTransferProtocolValidation(unittest.TestCase):
166193
def assertTabPyAppRaisesRuntimeError(self, expected_message):

tests/unit/server_tests/test_service_info_handler.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,9 @@ def test_given_server_with_auth_expect_correct_info_response(self):
100100
self.assertTrue("features" in v1)
101101
features = v1["features"]
102102
self.assertDictEqual(
103-
{"authentication": {"methods": {"basic-auth": {}}, "required": True}, 'evaluate_enabled': True},
104-
features,
103+
{"authentication": {"methods": {"basic-auth": {}}, "required": True},
104+
'evaluate_enabled': True, 'gzip_enabled': True},
105+
features,
105106
)
106107

107108

@@ -126,7 +127,7 @@ def test_server_with_no_auth_expect_correct_info_response(self):
126127
v1 = versions["v1"]
127128
self.assertTrue("features" in v1)
128129
features = v1["features"]
129-
self.assertDictEqual({'evaluate_enabled': True}, features)
130+
self.assertDictEqual({'evaluate_enabled': True, 'gzip_enabled': True}, features)
130131

131132
def test_given_server_with_no_auth_and_password_expect_correct_info_response(self):
132133
header = {

0 commit comments

Comments
 (0)