├── test ├── case1 │ ├── main.py │ ├── request.json │ ├── addons.py │ ├── reqable_main_test.py │ └── response.json ├── case2 │ ├── main.py │ ├── request.json │ ├── addons.py │ ├── reqable_main_test.py │ └── response.json ├── case3 │ ├── main.py │ ├── addons.py │ ├── reqable_main_test.py │ ├── request.json │ └── response.json ├── case4 │ ├── main.py │ ├── reqable_main_test.py │ ├── addons.py │ └── request.json ├── data │ ├── body_text.json │ └── body_binary.bin ├── test.sh ├── reqable_context_test.py ├── reqable_multipart_body_test.py ├── reqable_header_test.py ├── reqable_query_test.py ├── reqable_response_test.py ├── reqable_request_test.py └── reqable_body_test.py ├── reqable ├── __init__.py ├── addons_mini.py ├── addons.py ├── main.py └── reqable.py ├── upload.sh ├── .gitignore ├── README.md └── setup.py /test/case1/main.py: -------------------------------------------------------------------------------- 1 | ../../reqable/main.py -------------------------------------------------------------------------------- /test/case2/main.py: -------------------------------------------------------------------------------- 1 | ../../reqable/main.py -------------------------------------------------------------------------------- /test/case3/main.py: -------------------------------------------------------------------------------- 1 | ../../reqable/main.py -------------------------------------------------------------------------------- /test/case4/main.py: -------------------------------------------------------------------------------- 1 | ../../reqable/main.py -------------------------------------------------------------------------------- /reqable/__init__.py: -------------------------------------------------------------------------------- 1 | from reqable.reqable import * 2 | -------------------------------------------------------------------------------- /upload.sh: -------------------------------------------------------------------------------- 1 | python3 setup.py bdist_wheel 2 | twine upload dist/* -------------------------------------------------------------------------------- /test/data/body_text.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": "bar", 3 | "abc": 123, 4 | "hello": "world" 5 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | /build 3 | /dist 4 | /reqable_scripting.egg-info 5 | __pycache__ 6 | *.cb -------------------------------------------------------------------------------- /test/data/body_binary.bin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reqable/python-scripting-api/HEAD/test/data/body_binary.bin -------------------------------------------------------------------------------- /reqable/addons_mini.py: -------------------------------------------------------------------------------- 1 | # API Docs: https://reqable.com/docs/capture/addons 2 | 3 | from reqable import * 4 | 5 | def onRequest(context, request): 6 | return request 7 | 8 | def onResponse(context, response): 9 | return response -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Reqable Scripting Framework 2 | 3 | This is the Python scripting framework for Reqable. 4 | 5 | # Installation 6 | ``` 7 | pip3 install reqable-scripting 8 | ``` 9 | 10 | # Documentation 11 | 12 | https://reqable.com/docs/capture/addons -------------------------------------------------------------------------------- /test/case3/addons.py: -------------------------------------------------------------------------------- 1 | # API Docs: https://reqable.com/docs/capture/addons 2 | 3 | from reqable import * 4 | 5 | def onRequest(context, request): 6 | context.shared = 1 7 | # Done 8 | return request 9 | 10 | def onResponse(context, response): 11 | context.shared = 2 12 | # Done 13 | return response 14 | -------------------------------------------------------------------------------- /test/test.sh: -------------------------------------------------------------------------------- 1 | for file in $( find . -type f -name '*_test.py' ); 2 | do 3 | echo "Running test $file" 4 | dir=$(dirname "$file") 5 | if [ $dir == "." ]; then 6 | export PYTHONPATH="../reqable" 7 | python3 $file 8 | else 9 | cd $dir 10 | export PYTHONPATH="../../reqable" 11 | python3 $(basename "$file") 12 | cd .. 13 | fi; 14 | done 15 | -------------------------------------------------------------------------------- /test/case4/reqable_main_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import json 3 | import main 4 | 5 | class MainTest(unittest.TestCase): 6 | def testRequest(self): 7 | main.onRequest('request.json') 8 | with open('request.json.cb', 'r', encoding='UTF-8') as content: 9 | request = json.load(content)['request'] 10 | self.assertEqual(request['headers'][-1], 'signature: 3e8c6c3b1cdca44384d0beaa487dbd21') 11 | 12 | if __name__ == '__main__': 13 | unittest.main() -------------------------------------------------------------------------------- /test/case4/addons.py: -------------------------------------------------------------------------------- 1 | # API Docs: https://reqable.com/docs/capture/addons 2 | 3 | from reqable import * 4 | import hashlib 5 | 6 | def onRequest(context, request): 7 | queries = sorted(request.queries) 8 | text = '&'.join(['='.join(query) for query in queries]) 9 | algorithm = hashlib.md5() 10 | algorithm.update(text.encode(encoding='UTF-8')) 11 | signature = algorithm.hexdigest() 12 | request.headers['signature'] = signature 13 | # Done 14 | return request 15 | 16 | def onResponse(context, response): 17 | # Done 18 | return response 19 | -------------------------------------------------------------------------------- /test/case3/reqable_main_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import json 3 | import main 4 | 5 | class MainTest(unittest.TestCase): 6 | def testRequest(self): 7 | main.onRequest('request.json') 8 | with open('request.json.cb', 'r', encoding='UTF-8') as content: 9 | shared = json.load(content)['shared'] 10 | self.assertEqual(shared, 1) 11 | 12 | def testResponse(self): 13 | main.onResponse('response.json') 14 | with open('response.json.cb', 'r', encoding='UTF-8') as content: 15 | shared = json.load(content)['shared'] 16 | self.assertEqual(shared, 2) 17 | 18 | 19 | if __name__ == '__main__': 20 | unittest.main() -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | with open("README.md", "r") as fh: 4 | long_description = fh.read() 5 | 6 | setuptools.setup( 7 | name="reqable-scripting", 8 | version="1.2.0", 9 | author="MegatronKing", 10 | author_email="coding@reqable.com", 11 | url="https://reqable.com/docs/capture/addons", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | packages=setuptools.find_packages(), 15 | license="MIT", 16 | classifiers=[ 17 | "Programming Language :: Python :: 3", 18 | "License :: OSI Approved :: MIT License", 19 | "Operating System :: OS Independent", 20 | ], 21 | python_requires=">=3.6" 22 | ) 23 | -------------------------------------------------------------------------------- /test/case1/request.json: -------------------------------------------------------------------------------- 1 | {"context":{"url":"https://reqable.com/assets/js/main.js","scheme":"https","host":"reqable.com","port":443,"cid":37,"ctime":1686556321938,"sid":5,"stime":1686556322722,"shared":null},"request":{"method":"GET","path":"/assets/js/main.js","protocol":"HTTP/1.1","headers":["Host: reqable.com","Sec-Fetch-Site: same-origin","Accept-Encoding: gzip, deflate, br","Connection: keep-alive","Sec-Fetch-Mode: cors","Accept: */*","User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15","Referer: https://reqable.com/","Sec-Fetch-Dest: empty","Accept-Language: zh-CN,zh-Hans;q=0.9"],"body":{"type":0,"payload":null},"trailers":[]}} -------------------------------------------------------------------------------- /test/case2/request.json: -------------------------------------------------------------------------------- 1 | {"context":{"url":"https://reqable.com/assets/js/main.js","scheme":"https","host":"reqable.com","port":443,"cid":37,"ctime":1686556321938,"sid":5,"stime":1686556322722,"shared":null},"request":{"method":"GET","path":"/assets/js/main.js","protocol":"HTTP/1.1","headers":["Host: reqable.com","Sec-Fetch-Site: same-origin","Accept-Encoding: gzip, deflate, br","Connection: keep-alive","Sec-Fetch-Mode: cors","Accept: */*","User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15","Referer: https://reqable.com/","Sec-Fetch-Dest: empty","Accept-Language: zh-CN,zh-Hans;q=0.9"],"body":{"type":0,"payload":null},"trailers":[]}} -------------------------------------------------------------------------------- /test/case3/request.json: -------------------------------------------------------------------------------- 1 | {"context":{"url":"https://reqable.com/assets/js/main.js","scheme":"https","host":"reqable.com","port":443,"cid":37,"ctime":1686556321938,"sid":5,"stime":1686556322722,"shared":null},"request":{"method":"GET","path":"/assets/js/main.js","protocol":"HTTP/1.1","headers":["Host: reqable.com","Sec-Fetch-Site: same-origin","Accept-Encoding: gzip, deflate, br","Connection: keep-alive","Sec-Fetch-Mode: cors","Accept: */*","User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15","Referer: https://reqable.com/","Sec-Fetch-Dest: empty","Accept-Language: zh-CN,zh-Hans;q=0.9"],"body":{"type":0,"payload":null},"trailers":[]}} -------------------------------------------------------------------------------- /test/case4/request.json: -------------------------------------------------------------------------------- 1 | {"context":{"url":"https://reqable.com/assets/js/main.js?hello=world&reqable=awesome&name=megatronking","scheme":"https","host":"reqable.com","port":443,"cid":37,"ctime":1686556321938,"sid":5,"stime":1686556322722,"shared":null},"request":{"method":"GET","path":"/assets/js/main.js?hello=world&reqable=awesome&name=megatronking","protocol":"HTTP/1.1","headers":["Host: reqable.com","Sec-Fetch-Site: same-origin","Accept-Encoding: gzip, deflate, br","Connection: keep-alive","Sec-Fetch-Mode: cors","Accept: */*","User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15","Referer: https://reqable.com/","Sec-Fetch-Dest: empty","Accept-Language: zh-CN,zh-Hans;q=0.9"],"body":{"type":0,"payload":null},"trailers":[]}} -------------------------------------------------------------------------------- /test/case2/addons.py: -------------------------------------------------------------------------------- 1 | # API Docs: https://reqable.com/docs/capture/addons 2 | 3 | from reqable import * 4 | 5 | def onRequest(context, request): 6 | # Print url to console 7 | # print('request url ' + context.url) 8 | 9 | request.method = 'HEAD' 10 | request.path = '/abc' 11 | request.queries['foo'] = 'bar' 12 | request.headers['foo'] = 'bar' 13 | request.trailers['foo'] = 'bar' 14 | request.body = 'foobar' 15 | 16 | # Done 17 | return request 18 | 19 | def onResponse(context, response): 20 | # Update status code 21 | # response.code = 404 22 | 23 | # APIs are same as `onRequest` 24 | response.code = 404 25 | response.headers['foo'] = 'bar' 26 | response.trailers['foo'] = 'bar' 27 | response.body = 'foobar' 28 | 29 | 30 | # Done 31 | return response 32 | -------------------------------------------------------------------------------- /reqable/addons.py: -------------------------------------------------------------------------------- 1 | # API Docs: https://reqable.com/docs/capture/addons 2 | 3 | from reqable import * 4 | 5 | def onRequest(context, request): 6 | # Print url to console 7 | # print('request url ' + context.url) 8 | 9 | # Update or add a query parameter 10 | # request.queries['foo'] = 'bar' 11 | 12 | # Update or add a http header 13 | # request.headers['foo'] = 'bar' 14 | 15 | # Replace http body with a text 16 | # request.body = 'Hello World' 17 | 18 | # Map with a local file 19 | # request.body.file('~/Desktop/body.json') 20 | 21 | # Convert to dict if the body is a JSON 22 | # request.body.jsonify() 23 | # Update the JSON content 24 | # request.body['foo'] = 'bar' 25 | 26 | # Done 27 | return request 28 | 29 | def onResponse(context, response): 30 | # Update status code 31 | # response.code = 404 32 | 33 | # APIs are same as `onRequest` 34 | 35 | # Done 36 | return response 37 | -------------------------------------------------------------------------------- /test/case1/addons.py: -------------------------------------------------------------------------------- 1 | # API Docs: https://reqable.com/docs/capture/addons 2 | 3 | from reqable import * 4 | 5 | def onRequest(context, request): 6 | # Print url to console 7 | # print('request url ' + context.url) 8 | 9 | # Update or add a query parameter 10 | # request.queries['foo'] = 'bar' 11 | 12 | # Update or add http header 13 | # request.headers['foo'] = 'bar' 14 | 15 | # Replace http body with a text 16 | # request.body = 'Hello World' 17 | 18 | # Map with a local file 19 | # request.body.file('~/Desktop/body.json') 20 | 21 | # Convert to dict if the body is a JSON 22 | # request.body.jsonify() 23 | # Update the JSON content 24 | # request.body['foo'] = 'bar' 25 | 26 | # Done 27 | return request 28 | 29 | def onResponse(context, response): 30 | # Update status code 31 | # response.code = 404 32 | 33 | # APIs are same as `onRequest` 34 | 35 | # Done 36 | return response 37 | -------------------------------------------------------------------------------- /test/case1/reqable_main_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import json 3 | import main 4 | 5 | class MainTest(unittest.TestCase): 6 | def testRequest(self): 7 | main.onRequest('request.json') 8 | with open('request.json', 'r', encoding='UTF-8') as content: 9 | dict1 = json.load(content) 10 | with open('request.json.cb', 'r', encoding='UTF-8') as content: 11 | dict2 = json.load(content) 12 | self.assertEqual(dict1['request'], dict2['request']) 13 | self.assertEqual(dict2['shared'], None) 14 | 15 | def testResponse(self): 16 | main.onResponse('response.json') 17 | with open('response.json', 'r', encoding='UTF-8') as content: 18 | dict1 = json.load(content) 19 | with open('response.json.cb', 'r', encoding='UTF-8') as content: 20 | dict2 = json.load(content) 21 | self.assertEqual(dict1['response'], dict2['response']) 22 | 23 | 24 | if __name__ == '__main__': 25 | unittest.main() -------------------------------------------------------------------------------- /test/case1/response.json: -------------------------------------------------------------------------------- 1 | {"context":{"url":"https://reqable.com/assets/js/main.js","scheme":"https","host":"reqable.com","port":443,"cid":37,"ctime":1686556321938,"sid":5,"stime":1686556322722,"shared":null},"response":{"request":{"method":"GET","path":"/assets/js/main.js","protocol":"HTTP/1.1","headers":["Host: reqable.com","Sec-Fetch-Site: same-origin","Accept-Encoding: gzip, deflate, br","Connection: keep-alive","Sec-Fetch-Mode: cors","Accept: */*","User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15","Referer: https://reqable.com/","Sec-Fetch-Dest: empty","Accept-Language: zh-CN,zh-Hans;q=0.9","Content-Length: 0"],"body":{"type":0,"payload":null},"trailers":[]},"protocol":"HTTP/1.1","code":200,"message":"OK","headers":["Last-Modified: Sun, 11 Jun 2023 09:19:48 GMT","Content-Encoding: gzip","Server: nginx/1.20.1","Date: Sun, 11 Jun 2023 09:27:55 GMT","Content-Type: application/javascript; charset=utf-8","Content-Disposition: inline; filename=\"main.js\"","Content-Length: 2335","Accept-Ranges: bytes","X-NWS-LOG-UUID: 16305921839023859003","Connection: keep-alive","X-Cache-Lookup: Cache Hit"],"body":{"type":1,"payload":"Hello World"},"trailers":[]}} -------------------------------------------------------------------------------- /test/case2/reqable_main_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import json 3 | import main 4 | 5 | class MainTest(unittest.TestCase): 6 | def testRequest(self): 7 | main.onRequest('request.json') 8 | with open('request.json.cb', 'r', encoding='UTF-8') as content: 9 | request = json.load(content)['request'] 10 | self.assertEqual(request['method'], 'HEAD') 11 | self.assertEqual(request['path'], '/abc?foo=bar') 12 | self.assertEqual(request['headers'][-1], 'foo: bar') 13 | self.assertEqual(request['trailers'][-1], 'foo: bar') 14 | self.assertEqual(request['body']['type'], 1) 15 | self.assertEqual(request['body']['payload'], 'foobar') 16 | 17 | def testResponse(self): 18 | main.onResponse('response.json') 19 | with open('response.json.cb', 'r', encoding='UTF-8') as content: 20 | response = json.load(content)['response'] 21 | self.assertEqual(response['code'], 404) 22 | self.assertEqual(response['headers'][-1], 'foo: bar') 23 | self.assertEqual(response['trailers'][-1], 'foo: bar') 24 | self.assertEqual(response['body']['type'], 1) 25 | self.assertEqual(response['body']['payload'], 'foobar') 26 | 27 | 28 | if __name__ == '__main__': 29 | unittest.main() -------------------------------------------------------------------------------- /test/case2/response.json: -------------------------------------------------------------------------------- 1 | {"context":{"url":"https://reqable.com/assets/js/main.js","scheme":"https","host":"reqable.com","port":443,"cid":37,"ctime":1686556321938,"sid":5,"stime":1686556322722,"shared":null},"response":{"request":{"method":"GET","path":"/assets/js/main.js","protocol":"HTTP/1.1","headers":["Host: reqable.com","Sec-Fetch-Site: same-origin","Accept-Encoding: gzip, deflate, br","Connection: keep-alive","Sec-Fetch-Mode: cors","Accept: */*","User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15","Referer: https://reqable.com/","Sec-Fetch-Dest: empty","Accept-Language: zh-CN,zh-Hans;q=0.9","Content-Length: 0"],"body":{"type":0,"payload":null},"trailers":[]},"protocol":"HTTP/1.1","code":200,"message":"OK","headers":["Last-Modified: Sun, 11 Jun 2023 09:19:48 GMT","Content-Encoding: gzip","Server: nginx/1.20.1","Date: Sun, 11 Jun 2023 09:27:55 GMT","Content-Type: application/javascript; charset=utf-8","Content-Disposition: inline; filename=\"main.js\"","Content-Length: 2335","Accept-Ranges: bytes","X-NWS-LOG-UUID: 16305921839023859003","Connection: keep-alive","X-Cache-Lookup: Cache Hit"],"body":{"type":1,"payload":"Hello World"},"trailers":[]}} -------------------------------------------------------------------------------- /test/case3/response.json: -------------------------------------------------------------------------------- 1 | {"context":{"url":"https://reqable.com/assets/js/main.js","scheme":"https","host":"reqable.com","port":443,"cid":37,"ctime":1686556321938,"sid":5,"stime":1686556322722,"shared":null},"response":{"request":{"method":"GET","path":"/assets/js/main.js","protocol":"HTTP/1.1","headers":["Host: reqable.com","Sec-Fetch-Site: same-origin","Accept-Encoding: gzip, deflate, br","Connection: keep-alive","Sec-Fetch-Mode: cors","Accept: */*","User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.5 Safari/605.1.15","Referer: https://reqable.com/","Sec-Fetch-Dest: empty","Accept-Language: zh-CN,zh-Hans;q=0.9","Content-Length: 0"],"body":{"type":0,"payload":null},"trailers":[]},"protocol":"HTTP/1.1","code":200,"message":"OK","headers":["Last-Modified: Sun, 11 Jun 2023 09:19:48 GMT","Content-Encoding: gzip","Server: nginx/1.20.1","Date: Sun, 11 Jun 2023 09:27:55 GMT","Content-Type: application/javascript; charset=utf-8","Content-Disposition: inline; filename=\"main.js\"","Content-Length: 2335","Accept-Ranges: bytes","X-NWS-LOG-UUID: 16305921839023859003","Connection: keep-alive","X-Cache-Lookup: Cache Hit"],"body":{"type":1,"payload":"Hello World"},"trailers":[]}} -------------------------------------------------------------------------------- /test/reqable_context_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from reqable import CaptureContext 4 | 5 | class ContextTest(unittest.TestCase): 6 | def testContextConstructor(self): 7 | context = CaptureContext({ 8 | 'url': 'https://reqable.com', 9 | 'scheme': 'https', 10 | 'host': 'reqable.com', 11 | 'port': 443, 12 | 'cid': 32, 13 | 'ctime': 1686556178335, 14 | 'sid': 7, 15 | 'stime': 1686556256263, 16 | 'env': { 17 | 'foo': 'bar', 18 | 'abc': '123', 19 | '$randomEmail': 'random@reqable.com' 20 | } 21 | }) 22 | self.assertEqual(context.url, 'https://reqable.com') 23 | self.assertEqual(context.scheme, 'https') 24 | self.assertEqual(context.host, 'reqable.com') 25 | self.assertEqual(context.port, 443) 26 | self.assertEqual(context.cid, 32) 27 | self.assertEqual(context.ctime, 1686556178335) 28 | self.assertEqual(context.sid, 7) 29 | self.assertEqual(context.stime, 1686556256263) 30 | self.assertEqual(context.uid, '1686556178335-32-7') 31 | self.assertEqual(context.env['foo'], 'bar') 32 | self.assertEqual(context.env['abc'], '123') 33 | self.assertEqual(context.env['$randomEmail'], 'random@reqable.com') 34 | 35 | if __name__ == '__main__': 36 | unittest.main() -------------------------------------------------------------------------------- /reqable/main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | 4 | pwd = os.path.dirname(os.path.abspath(__file__)) 5 | if pwd not in sys.path: 6 | sys.path.append(pwd) 7 | 8 | import json 9 | from reqable import CaptureContext, CaptureHttpRequest, CaptureHttpResponse 10 | import addons 11 | 12 | def main(): 13 | argv = sys.argv[1:] 14 | if len(argv) != 2: 15 | raise Exception('Invalid reqable script arguments') 16 | type = argv[0] 17 | if type == 'request': 18 | onRequest(argv[1]) 19 | elif type == 'response': 20 | onResponse(argv[1]) 21 | else: 22 | raise Exception('Unexpected type ' + type) 23 | 24 | def onRequest(request): 25 | with open(request, 'r', encoding='UTF-8') as content: 26 | data = json.load(content) 27 | context = CaptureContext(data['context']) 28 | result = addons.onRequest(context, CaptureHttpRequest(data['request'])) 29 | if result is not None: 30 | with open(request + '.cb', 'w', encoding='UTF-8') as callback: 31 | callback.write(json.dumps({ 32 | 'request': result.serialize(), 33 | 'env': context.env, 34 | 'highlight': context.highlight, 35 | 'comment': context.comment, 36 | 'shared': context.shared, 37 | })) 38 | 39 | def onResponse(response): 40 | with open(response, 'r', encoding='UTF-8') as content: 41 | data = json.load(content) 42 | context = CaptureContext(data['context']) 43 | result = addons.onResponse(context, CaptureHttpResponse(data['response'])) 44 | if result is not None: 45 | with open(response + '.cb', 'w', encoding='UTF-8') as callback: 46 | callback.write(json.dumps({ 47 | 'response': result.serialize(), 48 | 'env': context.env, 49 | 'highlight': context.highlight, 50 | 'comment': context.comment, 51 | 'shared': context.shared, 52 | })) 53 | 54 | if __name__== '__main__': 55 | main() -------------------------------------------------------------------------------- /test/reqable_multipart_body_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from reqable import CaptureHttpMultipartBody as multipart 4 | 5 | class HttpMultipartBodyTest(unittest.TestCase): 6 | def testHttpMultipartBodyConstructor(self): 7 | body = multipart({ 8 | 'headers': [ 9 | 'foo: bar', 10 | 'abc: 123', 11 | 'hello: world' 12 | ], 13 | 'body': { 14 | 'type': 1, 15 | 'payload': 'Hello World' 16 | } 17 | }) 18 | self.assertEqual(body.type, 1) 19 | self.assertEqual(body.payload, 'Hello World') 20 | self.assertEqual(body.headers.entries, [ 21 | 'foo: bar', 22 | 'abc: 123', 23 | 'hello: world' 24 | ]) 25 | 26 | body = multipart.text('Hi World', 'python', 'image.png', [ 27 | 'abc: 123', 28 | 'foo: bar' 29 | ]) 30 | self.assertEqual(body.type, 1) 31 | self.assertEqual(body.payload, 'Hi World') 32 | self.assertEqual(body.headers.entries, [ 33 | 'abc: 123', 34 | 'foo: bar', 35 | 'content-length: 8', 36 | 'content-disposition: form-data; name="python"; filename="image.png"' 37 | ]) 38 | 39 | body = multipart.text('Hi World') 40 | self.assertEqual(body.headers.entries, [ 41 | 'content-length: 8', 42 | ]) 43 | 44 | body = multipart.text('Hi World', name = 'python') 45 | self.assertEqual(body.headers.entries, [ 46 | 'content-length: 8', 47 | 'content-disposition: form-data; name="python"' 48 | ]) 49 | 50 | body = multipart.text('Hi World', filename = 'image.png') 51 | self.assertEqual(body.headers.entries, [ 52 | 'content-length: 8', 53 | 'content-disposition: form-data; filename="image.png"' 54 | ]) 55 | 56 | body = multipart.file('data/body_binary.bin', 'python', 'image.png', [ 57 | 'abc: 123', 58 | 'foo: bar' 59 | ]) 60 | self.assertEqual(body.type, 2) 61 | self.assertEqual(body.payload, b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A') 62 | self.assertEqual(body.headers.entries, [ 63 | 'abc: 123', 64 | 'foo: bar', 65 | 'content-length: 8', 66 | 'content-disposition: form-data; name="python"; filename="image.png"' 67 | ]) 68 | 69 | 70 | def testHttpMultipartBodySerialize(self): 71 | data = { 72 | 'headers': [ 73 | 'foo: bar', 74 | 'abc: 123', 75 | 'hello: world' 76 | ], 77 | 'body': { 78 | 'type': 1, 79 | 'payload': 'Hello World' 80 | } 81 | } 82 | body = multipart(data) 83 | self.assertEqual(body.serialize(), data) 84 | 85 | 86 | if __name__ == '__main__': 87 | unittest.main() -------------------------------------------------------------------------------- /test/reqable_header_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from reqable import CaptureHttpHeaders 4 | 5 | class HttpHeadersTest(unittest.TestCase): 6 | def testHttpHeadersConstructor(self): 7 | headers = CaptureHttpHeaders() 8 | self.assertEqual(len(headers), 0) 9 | self.assertEqual(headers.entries, []) 10 | 11 | headers = CaptureHttpHeaders(None) 12 | self.assertEqual(len(headers), 0) 13 | self.assertEqual(headers.entries, []) 14 | 15 | headers = CaptureHttpHeaders([ 16 | 'foo: bar', 17 | 'abc: 123', 18 | 'hello: world' 19 | ]) 20 | self.assertEqual(len(headers), 3) 21 | self.assertEqual(headers['foo'], 'bar') 22 | self.assertEqual(headers['abc'], '123') 23 | self.assertEqual(headers['hello'], 'world') 24 | self.assertEqual(headers['python'], None) 25 | 26 | headers = CaptureHttpHeaders.of([ 27 | 'foo: bar', 28 | 'abc: 123', 29 | 'hello: world' 30 | ]) 31 | self.assertEqual(len(headers), 3) 32 | self.assertEqual(headers['foo'], 'bar') 33 | self.assertEqual(headers['abc'], '123') 34 | self.assertEqual(headers['hello'], 'world') 35 | 36 | headers = CaptureHttpHeaders.of({ 37 | 'foo': 'bar', 38 | 'abc': '123', 39 | 'hello': 'world' 40 | }) 41 | self.assertEqual(len(headers), 3) 42 | self.assertEqual(headers['foo'], 'bar') 43 | self.assertEqual(headers['abc'], '123') 44 | self.assertEqual(headers['hello'], 'world') 45 | 46 | headers = CaptureHttpHeaders.of([ 47 | ('foo', 'bar'), 48 | ('abc', '123'), 49 | ('hello', 'world') 50 | ]) 51 | self.assertEqual(len(headers), 3) 52 | self.assertEqual(headers['foo'], 'bar') 53 | self.assertEqual(headers['abc'], '123') 54 | self.assertEqual(headers['hello'], 'world') 55 | 56 | 57 | def testHttpHeadersUpdate(self): 58 | headers = CaptureHttpHeaders([ 59 | 'foo: bar', 60 | 'abc: 123', 61 | 'hello: world' 62 | ]) 63 | headers.add('python', 'good') 64 | self.assertEqual(headers['python'], 'good') 65 | self.assertEqual(len(headers), 4) 66 | 67 | headers = CaptureHttpHeaders([ 68 | 'foo: bar', 69 | 'abc: 123', 70 | 'hello: world' 71 | ]) 72 | headers['python'] = 'good' 73 | self.assertEqual(headers['python'], 'good') 74 | self.assertEqual(len(headers), 4) 75 | 76 | headers = CaptureHttpHeaders([ 77 | 'foo: bar', 78 | 'abc: 123', 79 | 'hello: world' 80 | ]) 81 | headers['foo'] = 'reqable' 82 | self.assertEqual(headers['foo'], 'reqable') 83 | self.assertEqual(len(headers), 3) 84 | 85 | headers = CaptureHttpHeaders([ 86 | 'foo: bar', 87 | 'abc: 123', 88 | 'hello: world' 89 | ]) 90 | headers.remove('foo') 91 | self.assertEqual(headers['foo'], None) 92 | self.assertEqual(len(headers), 2) 93 | 94 | headers = CaptureHttpHeaders([ 95 | 'foo: bar', 96 | 'abc: 123', 97 | 'hello: world' 98 | ]) 99 | headers.clear() 100 | self.assertEqual(len(headers), 0) 101 | 102 | def testHttpQueriesIndex(self): 103 | headers = CaptureHttpHeaders([ 104 | 'foo: bar', 105 | 'abc: 123', 106 | 'hello: world', 107 | ]) 108 | self.assertEqual(headers.index('foo'), 0) 109 | self.assertEqual(headers.index('abc'), 1) 110 | self.assertEqual(headers.index('hello'), 2) 111 | 112 | self.assertEqual(headers[0], 'foo: bar') 113 | self.assertEqual(headers[1], 'abc: 123') 114 | self.assertEqual(headers[2], 'hello: world') 115 | 116 | 117 | def testHttpHeadersDunplicateName(self): 118 | headers = CaptureHttpHeaders([ 119 | 'foo: bar', 120 | 'abc: 123', 121 | 'hello: world', 122 | 'foo: good', 123 | ]) 124 | self.assertEqual(headers.index('foo'), 0) 125 | self.assertEqual(headers.indexes('foo'), [0, 3]) 126 | self.assertEqual(headers['foo'], 'bar') 127 | 128 | 129 | def testHttpHeadersPrint(self): 130 | headers = CaptureHttpHeaders([ 131 | 'foo: bar', 132 | 'abc: 123', 133 | 'hello: world' 134 | ]) 135 | self.assertEqual(str(headers), "['foo: bar', 'abc: 123', 'hello: world']") 136 | 137 | 138 | def testHttpHeadersIterator(self): 139 | headers = CaptureHttpHeaders([ 140 | 'foo: bar', 141 | 'abc: 123', 142 | 'hello: world' 143 | ]) 144 | it = iter(headers) 145 | self.assertEqual(next(it), 'foo: bar') 146 | self.assertEqual(next(it), 'abc: 123') 147 | self.assertEqual(next(it), 'hello: world') 148 | 149 | 150 | def testHttpHeadersCasesensive(self): 151 | headers = CaptureHttpHeaders([ 152 | 'Foo: bar', 153 | 'Abc: 123', 154 | 'Hello: world' 155 | ]) 156 | self.assertEqual(headers['foo'], 'bar') 157 | self.assertEqual(headers['abc'], '123') 158 | self.assertEqual(headers['hello'], 'world') 159 | 160 | headers = CaptureHttpHeaders([ 161 | 'foo: bar', 162 | 'abc: 123', 163 | 'hello: world' 164 | ]) 165 | self.assertEqual(headers['Foo'], 'bar') 166 | self.assertEqual(headers['Abc'], '123') 167 | self.assertEqual(headers['Hello'], 'world') 168 | headers = CaptureHttpHeaders([ 169 | 'FOO: BAR', 170 | 'ABC: 123', 171 | 'HELLO: WORLD' 172 | ]) 173 | 174 | self.assertEqual(headers['foo'], 'BAR') 175 | self.assertEqual(headers['abc'], '123') 176 | self.assertEqual(headers['hello'], 'WORLD') 177 | 178 | def testHttpHeadersDict(self): 179 | headers = CaptureHttpHeaders([ 180 | 'foo: bar', 181 | 'abc: 123', 182 | 'hello: world', 183 | 'hello: ' 184 | ]) 185 | d = headers.toDict() 186 | self.assertEqual(d['foo'], 'bar') 187 | self.assertEqual(d['abc'], '123') 188 | self.assertEqual(d['hello'], '') 189 | 190 | if __name__ == '__main__': 191 | unittest.main() -------------------------------------------------------------------------------- /test/reqable_query_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from reqable import CaptureHttpQueries 4 | 5 | class HttpQueriesTest(unittest.TestCase): 6 | def testHttpQueriesConstructor(self): 7 | queries = CaptureHttpQueries() 8 | self.assertEqual(len(queries), 0) 9 | self.assertEqual(queries.entries, []) 10 | 11 | queries = CaptureHttpQueries([]) 12 | self.assertEqual(len(queries), 0) 13 | self.assertEqual(queries.entries, []) 14 | 15 | queries = CaptureHttpQueries(None) 16 | self.assertEqual(len(queries), 0) 17 | self.assertEqual(queries.entries, []) 18 | 19 | queries = CaptureHttpQueries.parse('foo=bar&abc=123&hello=world') 20 | self.assertEqual(len(queries), 3) 21 | self.assertEqual(queries['foo'], 'bar') 22 | self.assertEqual(queries['abc'], '123') 23 | self.assertEqual(queries['hello'], 'world') 24 | self.assertEqual(queries['python'], None) 25 | 26 | queries = CaptureHttpQueries.of('foo=bar&abc=123&hello=world') 27 | self.assertEqual(len(queries), 3) 28 | self.assertEqual(queries['foo'], 'bar') 29 | self.assertEqual(queries['abc'], '123') 30 | self.assertEqual(queries['hello'], 'world') 31 | 32 | queries = CaptureHttpQueries.parse('url=https%3A%2F%2Freqable.com') 33 | self.assertEqual(len(queries), 1) 34 | self.assertEqual(queries['url'], 'https://reqable.com') 35 | 36 | queries = CaptureHttpQueries.of('url=https%3A%2F%2Freqable.com') 37 | self.assertEqual(len(queries), 1) 38 | self.assertEqual(queries['url'], 'https://reqable.com') 39 | 40 | queries = CaptureHttpQueries.parse('foo') 41 | self.assertEqual(len(queries), 1) 42 | self.assertEqual(queries['foo'], '') 43 | 44 | queries = CaptureHttpQueries.parse('') 45 | self.assertEqual(len(queries), 0) 46 | 47 | queries = CaptureHttpQueries.of({ 48 | 'foo': 'bar', 49 | 'abc': '123' 50 | }) 51 | self.assertEqual(len(queries), 2) 52 | self.assertEqual(queries['foo'], 'bar') 53 | self.assertEqual(queries['abc'], '123') 54 | 55 | self.assertRaises(Exception, CaptureHttpQueries.of, { 56 | 'foo': 'bar', 57 | 'abc': 123 58 | }) 59 | 60 | queries = CaptureHttpQueries.of([ 61 | ('foo', 'bar'), 62 | ('abc', '123') 63 | ]) 64 | self.assertEqual(len(queries), 2) 65 | self.assertEqual(queries['foo'], 'bar') 66 | self.assertEqual(queries['abc'], '123') 67 | 68 | self.assertRaises(Exception, CaptureHttpQueries.of, { 69 | ('foo', 'bar'), 70 | ('abc', 123) 71 | }) 72 | 73 | 74 | def testHttpQueriesUpdate(self): 75 | queries = CaptureHttpQueries.parse('foo=bar&abc=123&hello=world') 76 | queries.add('python', 'good') 77 | self.assertEqual(queries['python'], 'good') 78 | self.assertEqual(len(queries), 4) 79 | 80 | queries = CaptureHttpQueries.parse('foo=bar&abc=123&hello=world') 81 | queries['python'] = 'good' 82 | self.assertEqual(queries['python'], 'good') 83 | self.assertEqual(len(queries), 4) 84 | 85 | queries = CaptureHttpQueries.parse('foo=bar&abc=123&hello=world') 86 | queries['foo'] = 'reqable' 87 | self.assertEqual(queries['foo'], 'reqable') 88 | self.assertEqual(len(queries), 3) 89 | 90 | queries = CaptureHttpQueries.parse('foo=bar&abc=123&hello=world') 91 | queries.remove('foo') 92 | self.assertEqual(queries['foo'], None) 93 | self.assertEqual(len(queries), 2) 94 | 95 | queries = CaptureHttpQueries.parse('foo=bar&abc=123&hello=world') 96 | queries.clear() 97 | self.assertEqual(len(queries), 0) 98 | 99 | 100 | def testHttpQueriesIterator(self): 101 | queries = CaptureHttpQueries.parse('foo=bar&abc=123&hello=world') 102 | it = iter(queries) 103 | self.assertEqual(next(it), ('foo', 'bar')) 104 | self.assertEqual(next(it), ('abc', '123')) 105 | self.assertEqual(next(it), ('hello', 'world')) 106 | 107 | 108 | def testHttpQueriesIndex(self): 109 | queries = CaptureHttpQueries.parse('foo=bar&abc=123&hello=world') 110 | self.assertEqual(queries.index('foo'), 0) 111 | self.assertEqual(queries.index('abc'), 1) 112 | self.assertEqual(queries.index('hello'), 2) 113 | 114 | self.assertEqual(queries[0], ('foo', 'bar')) 115 | self.assertEqual(queries[1], ('abc', '123')) 116 | self.assertEqual(queries[2], ('hello', 'world')) 117 | 118 | def testHttpQueriesDunplicateName(self): 119 | queries = CaptureHttpQueries.parse('foo=bar&abc=123&hello=world&foo=good') 120 | self.assertEqual(queries.index('foo'), 0) 121 | self.assertEqual(queries.indexes('foo'), [0, 3]) 122 | self.assertEqual(queries['foo'], 'bar') 123 | 124 | 125 | def testHttpQueriesPrint(self): 126 | queries = CaptureHttpQueries.parse('foo=bar&abc=123&hello=world') 127 | self.assertEqual(str(queries), "[('foo', 'bar'), ('abc', '123'), ('hello', 'world')]") 128 | 129 | 130 | def testHttpQueriesConcat(self): 131 | queries = CaptureHttpQueries.parse('foo=bar&abc=123&hello=world') 132 | self.assertEqual(queries.concat(), 'foo=bar&abc=123&hello=world') 133 | self.assertEqual(queries.concat(encode=True), 'foo=bar&abc=123&hello=world') 134 | 135 | queries = CaptureHttpQueries.parse('foo=bar&abc=123&url=https%3A%2F%2Freqable.com') 136 | self.assertEqual(queries.concat(), 'foo=bar&abc=123&url=https%3A%2F%2Freqable.com') 137 | self.assertEqual(queries.concat(encode=True), 'foo=bar&abc=123&url=https%3A%2F%2Freqable.com') 138 | self.assertEqual(queries.concat(encode=False), 'foo=bar&abc=123&url=https://reqable.com') 139 | 140 | def testHttpQueriesDict(self): 141 | queries = CaptureHttpQueries.parse('foo=bar&abc=123&hello=world&hello=') 142 | d = queries.toDict() 143 | self.assertEqual(d['foo'], 'bar') 144 | self.assertEqual(d['abc'], '123') 145 | self.assertEqual(d['hello'], '') 146 | 147 | if __name__ == '__main__': 148 | unittest.main() -------------------------------------------------------------------------------- /test/reqable_response_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from reqable import CaptureHttpResponse 4 | 5 | class HttpResponseTest(unittest.TestCase): 6 | def testHttpResponseConstructor(self): 7 | response = CaptureHttpResponse({ 8 | 'request': { 9 | 'method': 'GET', 10 | 'path': '/', 11 | 'protocol': 'HTTP/1.1', 12 | }, 13 | 'code': 200, 14 | 'message': 'OK', 15 | 'protocol': 'HTTP/1.1', 16 | }) 17 | self.assertEqual(response.code, 200) 18 | self.assertEqual(response.message, 'OK') 19 | self.assertEqual(response.protocol, 'HTTP/1.1') 20 | self.assertEqual(response.headers.entries, []) 21 | self.assertTrue(response.body.isNone) 22 | self.assertEqual(response.trailers.entries, []) 23 | 24 | response = CaptureHttpResponse({ 25 | 'request': { 26 | 'method': 'GET', 27 | 'path': '/', 28 | 'protocol': 'HTTP/1.1', 29 | }, 30 | 'code': 200, 31 | 'message': 'OK', 32 | 'protocol': 'HTTP/1.1', 33 | 'headers': [ 34 | 'foo: bar', 35 | 'abc: 123', 36 | 'hello: world' 37 | ], 38 | 'body': { 39 | 'type': 0, 40 | 'payload': None 41 | }, 42 | 'trailers': [ 43 | 'foo: bar', 44 | 'abc: 123', 45 | 'hello: world' 46 | ], 47 | }) 48 | self.assertEqual(response.code, 200) 49 | self.assertEqual(response.message, 'OK') 50 | self.assertEqual(response.protocol, 'HTTP/1.1') 51 | self.assertEqual(response.headers.entries, [ 52 | 'foo: bar', 53 | 'abc: 123', 54 | 'hello: world' 55 | ]) 56 | self.assertEqual(response.body.type, 0) 57 | self.assertEqual(response.body.payload, None) 58 | self.assertEqual(response.trailers.entries, [ 59 | 'foo: bar', 60 | 'abc: 123', 61 | 'hello: world' 62 | ]) 63 | 64 | 65 | def testHttpResponseCodeUpdate(self): 66 | response = CaptureHttpResponse({ 67 | 'request': { 68 | 'method': 'GET', 69 | 'path': '/', 70 | 'protocol': 'HTTP/1.1', 71 | }, 72 | 'code': 200, 73 | 'message': 'OK', 74 | 'protocol': 'HTTP/1.1', 75 | }) 76 | response.code = 404 77 | self.assertEqual(response.code, 404) 78 | 79 | def testHttpResponseHeadersUpdate(self): 80 | response = CaptureHttpResponse({ 81 | 'request': { 82 | 'method': 'GET', 83 | 'path': '/', 84 | 'protocol': 'HTTP/1.1', 85 | }, 86 | 'code': 200, 87 | 'message': 'OK', 88 | 'protocol': 'HTTP/1.1', 89 | }) 90 | response.headers['foo'] = 'bar' 91 | self.assertEqual(response.headers.entries, [ 92 | 'foo: bar' 93 | ]) 94 | response.headers['name'] = 'megatron' 95 | self.assertEqual(response.headers.entries, [ 96 | 'foo: bar', 97 | 'name: megatron' 98 | ]) 99 | response.headers = [ 100 | 'foo: bar', 101 | 'abc: 123' 102 | ] 103 | self.assertEqual(response.headers.entries, [ 104 | 'foo: bar', 105 | 'abc: 123' 106 | ]) 107 | response.headers = { 108 | 'foo': 'bar', 109 | 'abc': '123', 110 | } 111 | self.assertEqual(response.headers.entries, [ 112 | 'foo: bar', 113 | 'abc: 123' 114 | ]) 115 | response.headers = [ 116 | ('foo', 'bar'), 117 | ('abc', '123'), 118 | ] 119 | self.assertEqual(response.headers.entries, [ 120 | 'foo: bar', 121 | 'abc: 123' 122 | ]) 123 | 124 | def testHttpResponseTrailersUpdate(self): 125 | response = CaptureHttpResponse({ 126 | 'request': { 127 | 'method': 'GET', 128 | 'path': '/', 129 | 'protocol': 'HTTP/1.1', 130 | }, 131 | 'code': 200, 132 | 'message': 'OK', 133 | 'protocol': 'HTTP/1.1', 134 | }) 135 | response.trailers['foo'] = 'bar' 136 | self.assertEqual(response.trailers.entries, [ 137 | 'foo: bar' 138 | ]) 139 | response.trailers['name'] = 'megatron' 140 | self.assertEqual(response.trailers.entries, [ 141 | 'foo: bar', 142 | 'name: megatron' 143 | ]) 144 | response.trailers = [ 145 | 'foo: bar', 146 | 'abc: 123' 147 | ] 148 | self.assertEqual(response.trailers.entries, [ 149 | 'foo: bar', 150 | 'abc: 123' 151 | ]) 152 | 153 | def testHttpResponseBodyUpdate(self): 154 | response = CaptureHttpResponse({ 155 | 'request': { 156 | 'method': 'GET', 157 | 'path': '/', 158 | 'protocol': 'HTTP/1.1', 159 | }, 160 | 'code': 200, 161 | 'message': 'OK', 162 | 'protocol': 'HTTP/1.1', 163 | }) 164 | response.body = 'Hello World' 165 | self.assertTrue(response.body.isText) 166 | self.assertEqual(response.body.payload, 'Hello World') 167 | 168 | response.body = { 169 | 'foo': 'bar', 170 | 'abc': 123 171 | } 172 | self.assertTrue(response.body.isText) 173 | self.assertEqual(response.body.payload, '{"foo": "bar", "abc": 123}') 174 | 175 | response.body = b'\x01\x02\x03\x04' 176 | self.assertTrue(response.body.isBinary) 177 | self.assertEqual(response.body.payload, b'\x01\x02\x03\x04') 178 | 179 | 180 | def testHttpResponseContentType(self): 181 | response = CaptureHttpResponse({ 182 | 'request': { 183 | 'method': 'GET', 184 | 'path': '/', 185 | 'protocol': 'HTTP/1.1', 186 | }, 187 | 'code': 200, 188 | 'message': 'OK', 189 | 'protocol': 'HTTP/1.1', 190 | 'headers': [ 191 | 'content-type: text/palin; charset=utf-8', 192 | ], 193 | }) 194 | self.assertEqual(response.contentType, 'text/palin; charset=utf-8') 195 | self.assertEqual(response.mime, 'text/palin') 196 | 197 | def testHttpResponseSerialize(self): 198 | data = { 199 | 'request': { 200 | 'method': 'GET', 201 | 'path': '/', 202 | 'protocol': 'HTTP/1.1', 203 | 'headers': [], 204 | 'body': { 205 | 'type': 0, 206 | 'payload': None 207 | }, 208 | 'trailers': [] 209 | }, 210 | 'code': 200, 211 | 'message': 'OK', 212 | 'protocol': 'HTTP/1.1', 213 | 'headers': [ 214 | 'foo: bar', 215 | 'abc: 123', 216 | 'hello: world' 217 | ], 218 | 'body': { 219 | 'type': 1, 220 | 'payload': 'Hello World' 221 | }, 222 | 'trailers': [] 223 | } 224 | response = CaptureHttpResponse(data) 225 | self.assertEqual(response.serialize(), data) 226 | 227 | response.code = 404 228 | response.headers.remove('foo') 229 | response.body = 'Reqable' 230 | response.trailers['abc'] = '123' 231 | self.assertEqual(response.serialize(), { 232 | 'request': { 233 | 'method': 'GET', 234 | 'path': '/', 235 | 'protocol': 'HTTP/1.1', 236 | 'headers': [], 237 | 'body': { 238 | 'type': 0, 239 | 'payload': None 240 | }, 241 | 'trailers': [] 242 | }, 243 | 'code': 404, 244 | 'message': 'OK', 245 | 'protocol': 'HTTP/1.1', 246 | 'headers': [ 247 | 'abc: 123', 248 | 'hello: world' 249 | ], 250 | 'body': { 251 | 'type': 1, 252 | 'payload': 'Reqable' 253 | }, 254 | 'trailers': [ 255 | 'abc: 123', 256 | ] 257 | }) 258 | 259 | if __name__ == '__main__': 260 | unittest.main() -------------------------------------------------------------------------------- /test/reqable_request_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from reqable import CaptureHttpRequest 4 | 5 | class HttpRequestTest(unittest.TestCase): 6 | def testHttpRequestConstructor(self): 7 | request = CaptureHttpRequest({ 8 | 'method': 'GET', 9 | 'path': '/', 10 | 'protocol': 'HTTP/1.1', 11 | 'headers': [ 12 | 'foo: bar', 13 | 'abc: 123', 14 | 'hello: world' 15 | ], 16 | 'body': { 17 | 'type': 0, 18 | 'payload': None 19 | }, 20 | }) 21 | self.assertEqual(request.method, 'GET') 22 | self.assertEqual(request.path, '/') 23 | self.assertEqual(request.protocol, 'HTTP/1.1') 24 | self.assertEqual(request.queries.entries, []) 25 | self.assertEqual(request.headers.entries, [ 26 | 'foo: bar', 27 | 'abc: 123', 28 | 'hello: world' 29 | ]) 30 | self.assertEqual(request.body.type, 0) 31 | self.assertEqual(request.body.payload, None) 32 | self.assertEqual(request.trailers.entries, []) 33 | 34 | request = CaptureHttpRequest({ 35 | 'method': 'POST', 36 | 'path': '/good?python=good&name=megatron', 37 | 'protocol': 'h2', 38 | 'trailers': [ 39 | 'foo: bar', 40 | 'abc: 123', 41 | 'hello: world' 42 | ] 43 | }) 44 | self.assertEqual(request.method, 'POST') 45 | self.assertEqual(request.path, '/good') 46 | self.assertEqual(request.protocol, 'h2') 47 | self.assertEqual(request.queries.entries, [ 48 | ('python', 'good'), 49 | ('name', 'megatron'), 50 | ]) 51 | self.assertEqual(request.headers.entries, []) 52 | self.assertEqual(request.trailers.entries, [ 53 | 'foo: bar', 54 | 'abc: 123', 55 | 'hello: world' 56 | ]) 57 | 58 | 59 | def testHttpRequestMethodUpdate(self): 60 | request = CaptureHttpRequest({ 61 | 'method': 'GET', 62 | 'path': '/', 63 | 'protocol': 'HTTP/1.1', 64 | }) 65 | 66 | request.method = 'DELETE' 67 | self.assertEqual(request.method, 'DELETE') 68 | 69 | 70 | def testHttpRequestPathUpdate(self): 71 | request = CaptureHttpRequest({ 72 | 'method': 'GET', 73 | 'path': '/', 74 | 'protocol': 'HTTP/1.1', 75 | }) 76 | request.path = '/abc' 77 | self.assertEqual(request.path, '/abc') 78 | 79 | 80 | def testHttpRequestQueriesUpdate(self): 81 | request = CaptureHttpRequest({ 82 | 'method': 'GET', 83 | 'path': '/', 84 | 'protocol': 'HTTP/1.1', 85 | }) 86 | request.queries['foo'] = 'bar' 87 | self.assertEqual(request.queries.entries, [ 88 | ('foo', 'bar'), 89 | ]) 90 | request = CaptureHttpRequest({ 91 | 'method': 'GET', 92 | 'path': '/good?python=good&name=megatron', 93 | 'protocol': 'HTTP/1.1', 94 | }) 95 | request.queries['foo'] = 'bar' 96 | self.assertEqual(request.queries.entries, [ 97 | ('python', 'good'), 98 | ('name', 'megatron'), 99 | ('foo', 'bar'), 100 | ]) 101 | 102 | request.queries.clear() 103 | self.assertEqual(request.queries.entries, []) 104 | 105 | request.queries = 'python=good&name=megatron' 106 | self.assertEqual(request.queries.entries, [ 107 | ('python', 'good'), 108 | ('name', 'megatron'), 109 | ]) 110 | 111 | request.queries = { 112 | 'foo': 'bar' 113 | } 114 | self.assertEqual(request.queries.entries, [ 115 | ('foo', 'bar'), 116 | ]) 117 | request.queries = [ 118 | ('foo', 'bar') 119 | ] 120 | self.assertEqual(request.queries.entries, [ 121 | ('foo', 'bar'), 122 | ]) 123 | 124 | 125 | def testHttpRequestHeadersUpdate(self): 126 | request = CaptureHttpRequest({ 127 | 'method': 'GET', 128 | 'path': '/', 129 | 'protocol': 'HTTP/1.1', 130 | }) 131 | request.headers['foo'] = 'bar' 132 | self.assertEqual(request.headers.entries, [ 133 | 'foo: bar' 134 | ]) 135 | request.headers['name'] = 'megatron' 136 | self.assertEqual(request.headers.entries, [ 137 | 'foo: bar', 138 | 'name: megatron' 139 | ]) 140 | request.headers = [ 141 | 'foo: bar', 142 | 'abc: 123' 143 | ] 144 | self.assertEqual(request.headers.entries, [ 145 | 'foo: bar', 146 | 'abc: 123' 147 | ]) 148 | request.headers = { 149 | 'foo': 'bar', 150 | 'abc': '123', 151 | } 152 | self.assertEqual(request.headers.entries, [ 153 | 'foo: bar', 154 | 'abc: 123' 155 | ]) 156 | request.headers = [ 157 | ('foo', 'bar'), 158 | ('abc', '123'), 159 | ] 160 | self.assertEqual(request.headers.entries, [ 161 | 'foo: bar', 162 | 'abc: 123' 163 | ]) 164 | 165 | def testHttpRequestTrailersUpdate(self): 166 | request = CaptureHttpRequest({ 167 | 'method': 'GET', 168 | 'path': '/', 169 | 'protocol': 'HTTP/1.1', 170 | }) 171 | request.trailers['foo'] = 'bar' 172 | self.assertEqual(request.trailers.entries, [ 173 | 'foo: bar' 174 | ]) 175 | request.trailers['name'] = 'megatron' 176 | self.assertEqual(request.trailers.entries, [ 177 | 'foo: bar', 178 | 'name: megatron' 179 | ]) 180 | request.trailers = [ 181 | 'foo: bar', 182 | 'abc: 123' 183 | ] 184 | self.assertEqual(request.trailers.entries, [ 185 | 'foo: bar', 186 | 'abc: 123' 187 | ]) 188 | 189 | def testHttpRequestBodyUpdate(self): 190 | request = CaptureHttpRequest({ 191 | 'method': 'GET', 192 | 'path': '/', 193 | 'protocol': 'HTTP/1.1', 194 | }) 195 | request.body = 'Hello World' 196 | self.assertTrue(request.body.isText) 197 | self.assertEqual(request.body.payload, 'Hello World') 198 | 199 | request.body = { 200 | 'foo': 'bar', 201 | 'abc': 123 202 | } 203 | self.assertTrue(request.body.isText) 204 | self.assertEqual(request.body.payload, '{"foo": "bar", "abc": 123}') 205 | 206 | request.body = b'\x01\x02\x03\x04' 207 | self.assertTrue(request.body.isBinary) 208 | self.assertEqual(request.body.payload, b'\x01\x02\x03\x04') 209 | 210 | 211 | def testHttpRequestContentType(self): 212 | request = CaptureHttpRequest({ 213 | 'method': 'GET', 214 | 'path': '/', 215 | 'protocol': 'HTTP/1.1', 216 | 'headers': [ 217 | 'content-type: text/palin; charset=utf-8', 218 | ], 219 | }) 220 | self.assertEqual(request.contentType, 'text/palin; charset=utf-8') 221 | self.assertEqual(request.mime, 'text/palin') 222 | 223 | def testHttpRequestSerialize(self): 224 | data = { 225 | 'method': 'GET', 226 | 'path': '/', 227 | 'protocol': 'HTTP/1.1', 228 | 'headers': [ 229 | 'foo: bar', 230 | 'abc: 123', 231 | 'hello: world' 232 | ], 233 | 'body': { 234 | 'type': 1, 235 | 'payload': 'Hello World' 236 | }, 237 | 'trailers': [] 238 | } 239 | request = CaptureHttpRequest(data) 240 | self.assertEqual(request.serialize(), data) 241 | 242 | request.method = 'POST' 243 | request.path = '/abc' 244 | request.queries['foo'] = 'bar' 245 | request.headers.remove('foo') 246 | request.body = 'Reqable' 247 | request.trailers['abc'] = '123' 248 | self.assertEqual(request.serialize(), { 249 | 'method': 'POST', 250 | 'path': '/abc?foo=bar', 251 | 'protocol': 'HTTP/1.1', 252 | 'headers': [ 253 | 'abc: 123', 254 | 'hello: world' 255 | ], 256 | 'body': { 257 | 'type': 1, 258 | 'payload': 'Reqable' 259 | }, 260 | 'trailers': [ 261 | 'abc: 123', 262 | ] 263 | }) 264 | 265 | if __name__ == '__main__': 266 | unittest.main() -------------------------------------------------------------------------------- /test/reqable_body_test.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from reqable import CaptureHttpBody, CaptureHttpMultipartBody 4 | 5 | class HttpBodyTest(unittest.TestCase): 6 | def testHttpBodyConstructor(self): 7 | body = CaptureHttpBody(0, None) 8 | self.assertEqual(body.type, 0) 9 | self.assertEqual(body.payload, None) 10 | 11 | body = CaptureHttpBody.of(None) 12 | self.assertEqual(body.type, 0) 13 | self.assertEqual(body.payload, None) 14 | 15 | body = CaptureHttpBody.of() 16 | self.assertEqual(body.type, 0) 17 | self.assertEqual(body.payload, None) 18 | 19 | body = CaptureHttpBody.parse({ 20 | 'type': 0, 21 | 'payload': None 22 | }) 23 | self.assertEqual(body.type, 0) 24 | self.assertEqual(body.payload, None) 25 | 26 | body = CaptureHttpBody(1, 'Hello World') 27 | self.assertEqual(body.type, 1) 28 | self.assertEqual(body.payload, 'Hello World') 29 | 30 | body = CaptureHttpBody.of('Hello World') 31 | self.assertEqual(body.type, 1) 32 | self.assertEqual(body.payload, 'Hello World') 33 | 34 | body = CaptureHttpBody.parse({ 35 | 'type': 1, 36 | 'payload': 'Hello World' 37 | }) 38 | self.assertEqual(body.type, 1) 39 | self.assertEqual(body.payload, 'Hello World') 40 | 41 | body = CaptureHttpBody(2, b'\x01\x02\x03\x04') 42 | self.assertEqual(body.type, 2) 43 | self.assertEqual(body.payload, b'\x01\x02\x03\x04') 44 | 45 | body = CaptureHttpBody.of(b'\x01\x02\x03\x04') 46 | self.assertEqual(body.type, 2) 47 | self.assertEqual(body.payload, b'\x01\x02\x03\x04') 48 | 49 | body = CaptureHttpBody.parse({ 50 | 'type': 2, 51 | 'payload': 'data/body_binary.bin' 52 | }) 53 | self.assertEqual(body.type, 2) 54 | self.assertEqual(body.payload, b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A') 55 | 56 | body = CaptureHttpBody.parse({ 57 | 'type': 3, 58 | 'payload': [ 59 | { 60 | 'headers': [ 61 | 'foo: bar', 62 | 'abc: 123', 63 | 'hello: world' 64 | ], 65 | 'body': { 66 | 'type': 1, 67 | 'payload': 'Hello World' 68 | } 69 | } 70 | ] 71 | }) 72 | self.assertEqual(body.type, 3) 73 | self.assertEqual(len(body.payload), 1) 74 | self.assertTrue(type(body.payload[0]) is CaptureHttpMultipartBody) 75 | 76 | body = CaptureHttpBody.of({ 77 | 'foo': 'bar', 78 | 'abc': 123, 79 | 'hello': 'world' 80 | }) 81 | self.assertEqual(body.type, 1) 82 | self.assertEqual(body.payload, '{"foo": "bar", "abc": 123, "hello": "world"}') 83 | 84 | 85 | def testHttpBodyNone(self): 86 | body = CaptureHttpBody() 87 | self.assertEqual(len(body), 0) 88 | self.assertEqual(str(body), '') 89 | self.assertTrue(body.isNone) 90 | self.assertFalse(body.isText) 91 | self.assertFalse(body.isBinary) 92 | self.assertFalse(body.isMultipart) 93 | 94 | self.assertEqual(body.serialize(), { 95 | 'type': 0, 96 | 'payload': None 97 | }) 98 | 99 | 100 | def testHttpBodyText(self): 101 | body = CaptureHttpBody.of('Hello World') 102 | self.assertEqual(len(body), len('Hello World')) 103 | self.assertEqual(str(body), 'Hello World') 104 | self.assertFalse(body.isNone) 105 | self.assertTrue(body.isText) 106 | self.assertFalse(body.isBinary) 107 | self.assertFalse(body.isMultipart) 108 | 109 | body.replace('Hello', 'Hi') 110 | self.assertEqual(body.payload, 'Hi World') 111 | self.assertEqual(body.serialize(), { 112 | 'type': 1, 113 | 'payload': 'Hi World' 114 | }) 115 | 116 | 117 | def testHttpBodyJson(self): 118 | body = CaptureHttpBody.of({"foo":"bar","abc":123,"hello":"world"}) 119 | body.jsonify() 120 | self.assertEqual(body['foo'], 'bar') 121 | self.assertEqual(body['abc'], 123) 122 | self.assertEqual(body['hello'], 'world') 123 | body['foo'] = 'good' 124 | self.assertEqual(body['foo'], 'good') 125 | body['python'] = { 126 | 'hi': 'reqable', 127 | } 128 | self.assertEqual(body['python'], { 129 | 'hi': 'reqable', 130 | }) 131 | body['python']['hi'] = 'megatron' 132 | self.assertEqual(body['python'], { 133 | 'hi': 'megatron', 134 | }) 135 | self.assertEqual(str(body), '{"foo": "good", "abc": 123, "hello": "world", "python": {"hi": "megatron"}}') 136 | self.assertEqual(body.serialize(), { 137 | 'type': 1, 138 | 'payload': '{"foo": "good", "abc": 123, "hello": "world", "python": {"hi": "megatron"}}' 139 | }) 140 | 141 | 142 | def testHttpBodyBinary(self): 143 | body = CaptureHttpBody.parse({ 144 | 'type': 2, 145 | 'payload': 'data/body_binary.bin' 146 | }) 147 | self.assertFalse(body.isNone) 148 | self.assertFalse(body.isText) 149 | self.assertTrue(body.isBinary) 150 | self.assertFalse(body.isMultipart) 151 | self.assertEqual(len(body), 8) 152 | self.assertEqual(body[0], 0x89) 153 | self.assertEqual(body[1], 0x50) 154 | # TODO test serialize 155 | 156 | 157 | def testHttpBodyMultipart(self): 158 | data = { 159 | 'type': 3, 160 | 'payload': [ 161 | { 162 | 'headers': [ 163 | 'foo: bar', 164 | 'abc: 123', 165 | 'hello: world' 166 | ], 167 | 'body': { 168 | 'type': 1, 169 | 'payload': 'Hello World' 170 | } 171 | } 172 | ] 173 | } 174 | body = CaptureHttpBody.parse({ 175 | 'type': 3, 176 | 'payload': [ 177 | { 178 | 'headers': [ 179 | 'foo: bar', 180 | 'abc: 123', 181 | 'hello: world' 182 | ], 183 | 'body': { 184 | 'type': 1, 185 | 'payload': 'Hello World' 186 | } 187 | } 188 | ] 189 | }) 190 | self.assertFalse(body.isNone) 191 | self.assertFalse(body.isText) 192 | self.assertFalse(body.isBinary) 193 | self.assertTrue(body.isMultipart) 194 | self.assertEqual(len(body), 1) 195 | self.assertTrue(isinstance(body[0], CaptureHttpMultipartBody)) 196 | self.assertEqual(body[0].headers.entries, [ 197 | 'foo: bar', 198 | 'abc: 123', 199 | 'hello: world' 200 | ]) 201 | self.assertEqual(body[0].type, 1) 202 | self.assertEqual(body[0].payload, 'Hello World') 203 | self.assertRaises(Exception, str, body) 204 | for part in body: 205 | self.assertEqual(body[0], part) 206 | self.assertEqual(body.serialize(), data) 207 | 208 | def testHttpBodyUpdate(self): 209 | body = CaptureHttpBody() 210 | body.text('Hello World') 211 | self.assertEqual(body.type, 1) 212 | self.assertEqual(body.payload, 'Hello World') 213 | 214 | body.textFromFile('data/body_text.json') 215 | self.assertEqual(body.type, 1) 216 | self.assertEqual(body.payload, '{\n "foo": "bar",\n "abc": 123,\n "hello": "world"\n}') 217 | 218 | body.file('data/body_binary.bin') 219 | self.assertEqual(body.type, 2) 220 | self.assertEqual(body.payload, b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A') 221 | 222 | body.binary('data/body_binary.bin') 223 | self.assertEqual(body.type, 2) 224 | self.assertEqual(body.payload, b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A') 225 | body.binary(b'\x01\x02\x03\x04') 226 | self.assertEqual(body.type, 2) 227 | self.assertEqual(body.payload, b'\x01\x02\x03\x04') 228 | 229 | body.multiparts([ 230 | CaptureHttpMultipartBody.text('Hi World'), 231 | CaptureHttpMultipartBody.file('data/body_binary.bin') 232 | ]) 233 | self.assertEqual(body.type, 3) 234 | self.assertTrue(body.payload[0].isText) 235 | self.assertEqual(body.payload[0].payload, 'Hi World') 236 | self.assertTrue(body.payload[1].isBinary) 237 | self.assertEqual(body.payload[1].payload, b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A') 238 | 239 | body.none() 240 | self.assertEqual(body.type, 0) 241 | self.assertEqual(body.payload, None) 242 | 243 | if __name__ == '__main__': 244 | unittest.main() -------------------------------------------------------------------------------- /reqable/reqable.py: -------------------------------------------------------------------------------- 1 | import json 2 | import uuid 3 | import os 4 | from email.message import EmailMessage 5 | from enum import Enum 6 | from typing import Union, List, Tuple, Dict 7 | 8 | class App: 9 | def __init__(self, json: dict): 10 | self._name = json['name'] 11 | self._id = json.get('id') 12 | self._path = json.get('path') 13 | 14 | # The app's name. 15 | @property 16 | def name(self) -> str: 17 | return self._name 18 | 19 | # The app unique id, such as `bundleId`, `packageName`. 20 | # return None if not detected. 21 | @property 22 | def id(self) -> Union[str, None]: 23 | return self._id 24 | 25 | # The app installed path. 26 | # return None if not detected. 27 | @property 28 | def path(self) -> Union[str, None]: 29 | return self._path 30 | 31 | # Serialize the app info to a dict. 32 | def serialize(self) -> dict: 33 | return { 34 | 'name': self._name, 35 | 'id': self._id, 36 | 'path': self._path, 37 | } 38 | 39 | class Highlight(Enum): 40 | none = 0 41 | red = 1 42 | yellow = 2 43 | green = 3 44 | blue = 4 45 | teal = 5 46 | strikethrough = 6 47 | 48 | class Context: 49 | def __init__(self, json: dict): 50 | self._url = json['url'] 51 | self._scheme = json['scheme'] 52 | self._host = json['host'] 53 | self._port = json['port'] 54 | self._cid = json['cid'] 55 | self._ctime = json['ctime'] 56 | self._sid = json['sid'] 57 | self._stime = json['stime'] 58 | self._env = json.get('env') 59 | self._comment = json.get('comment') 60 | app = json.get('app') 61 | if app is None: 62 | self._app = None 63 | else: 64 | self._app = App(app) 65 | self._highlight = None 66 | self.shared = json.get('shared') 67 | 68 | def __str__(self): 69 | return self.toJson() 70 | 71 | def __add__(self, other) -> str: 72 | return str(self) + other 73 | 74 | def __radd__(self, other) -> str: 75 | return other + str(self) 76 | 77 | # Request full URL. 78 | @property 79 | def url(self) -> str: 80 | return self._url 81 | 82 | # URL scheme, http or https. 83 | @property 84 | def scheme(self) -> str: 85 | return self._scheme 86 | 87 | # URL host. 88 | @property 89 | def host(self) -> str: 90 | return self._host 91 | 92 | # URL port 93 | @property 94 | def port(self) -> int: 95 | return self._port 96 | 97 | # TCP connection id. 98 | @property 99 | def cid(self) -> int: 100 | return self._cid 101 | 102 | # TCP connection establised timestamp. 103 | @property 104 | def ctime(self) -> int: 105 | return self._ctime 106 | 107 | # HTTP session id. 108 | @property 109 | def sid(self) -> int: 110 | return self._sid 111 | 112 | # HTTP session timestamp. 113 | @property 114 | def stime(self) -> int: 115 | return self._stime 116 | 117 | # HTTP uniqued id. 118 | @property 119 | def uid(self) -> str: 120 | return f"{self.ctime}-{self.cid}-{self.sid}" 121 | 122 | # Environment variables. 123 | @property 124 | def env(self) -> Dict[str, str]: 125 | return self._env 126 | 127 | # App info, return None means unknown app. 128 | @property 129 | def app(self) -> Union[App, None]: 130 | return self._app 131 | 132 | @property 133 | def highlight(self) -> Union[None, int]: 134 | return self._highlight 135 | 136 | # Set the highlight color. 137 | @highlight.setter 138 | def highlight(self, highlight: Highlight): 139 | self._highlight = highlight.value 140 | 141 | # Get the comment. 142 | @property 143 | def comment(self) -> Union[None, int]: 144 | return self._comment 145 | 146 | # Set the comment. 147 | @comment.setter 148 | def comment(self, comment: str): 149 | self._comment = comment 150 | 151 | def toJson(self) -> str: 152 | return json.dumps({ 153 | 'url': self._url, 154 | 'scheme': self._scheme, 155 | 'host': self._host, 156 | 'port': self._port, 157 | 'cid': self._cid, 158 | 'ctime': self._ctime, 159 | 'sid': self._sid, 160 | 'stime': self._stime, 161 | 'env': self._env, 162 | 'app': self._app.serialize(), 163 | 'shared': self.shared, 164 | 'highlight': self._highlight, 165 | 'comment': self._comment, 166 | }) 167 | 168 | class HttpQueries: 169 | 170 | def __init__(self, entries = None, origin = None): 171 | if entries is None: 172 | self._entries = [] 173 | else: 174 | self._entries = entries 175 | self.origin = origin 176 | self.mod = 0 177 | 178 | @classmethod 179 | def parse(cls, query: str): 180 | if query is None: 181 | entries = [] 182 | else: 183 | from urllib.parse import parse_qsl 184 | entries = parse_qsl(query, keep_blank_values = True) 185 | return cls(entries, query) 186 | 187 | @classmethod 188 | def of(cls, data): 189 | if isinstance(data, str): 190 | return cls.parse(data) 191 | elif isinstance(data, list) and all(isinstance(t, tuple) and isinstance(t[0], str) and 192 | isinstance(t[1], str) for t in data): 193 | return cls(data) 194 | elif isinstance(data, dict) and all(isinstance(key, str) and isinstance(value, str) 195 | for key, value in data.items()): 196 | entries = [] 197 | for key, value in data.items(): 198 | entries.append((key, value)) 199 | return cls(entries) 200 | raise Exception('Unsupported query parameters data type') 201 | 202 | def __len__(self): 203 | return len(self._entries) 204 | 205 | def __iter__(self): 206 | return iter(self._entries) 207 | 208 | def __str__(self): 209 | return str(self._entries) 210 | 211 | def __add__(self, other): 212 | return str(self) + other 213 | 214 | def __radd__(self, other): 215 | return other + str(self) 216 | 217 | def __getitem__(self, name: Union[str, int]) -> Union[str, None]: 218 | if isinstance(name, int): 219 | return self._entries[name] 220 | index = self.index(name) 221 | if index >= 0: 222 | return self._entries[index][1] 223 | return None 224 | 225 | def __setitem__(self, name: str, value: str): 226 | if len(name) >= 1: 227 | index = self.index(name) 228 | if index >= 0: 229 | self._entries[index] = (name, value) 230 | else: 231 | self._entries.append((name, value)) 232 | self.mod += 1 233 | 234 | # Add a query paramater with name and value. 235 | def add(self, name: str, value: str): 236 | if not name: 237 | return 238 | self._entries.append((name, value)) 239 | self.mod += 1 240 | 241 | # Remove query paramaters by name, all the matched query paramaters will be removed. 242 | def remove(self, name: str): 243 | for index in reversed(self.indexes(name)): 244 | self._entries.pop(index) 245 | self.mod += 1 246 | 247 | # Find the first query paramater index by name. If no matched, returns -1. 248 | def index(self, name: str) -> int: 249 | for i in range(len(self._entries)): 250 | if self._entries[i][0] == name: 251 | return i 252 | return -1 253 | 254 | # Find query paramater indexes by name. 255 | def indexes(self, name: str) -> List[int]: 256 | indexes = [] 257 | for i in range(len(self._entries)): 258 | if self._entries[i][0] == name: 259 | indexes.append(i) 260 | return indexes 261 | 262 | # Remove all query paramaters. 263 | def clear(self): 264 | self._entries.clear() 265 | self.mod += 1 266 | 267 | # Concat all the query paramaters to a query string. 268 | def concat(self, encode: bool = True) -> str: 269 | if encode: 270 | from urllib.parse import urlencode 271 | # Keep asterish to be safe 272 | return urlencode(self._entries, safe='*=') 273 | else: 274 | return '&'.join(['='.join(entry) for entry in self._entries]) 275 | 276 | # Get all query paramaters. 277 | @property 278 | def entries(self) -> List[str]: 279 | return self._entries 280 | 281 | # Convert the query paramaters to a dict. 282 | def toDict(self) -> Dict[str, str]: 283 | map = {} 284 | for entry in self.entries: 285 | map[entry[0]] = entry[1] 286 | return map 287 | 288 | # Convert the query paramaters to a json str. 289 | def toJson(self) -> str: 290 | return json.dumps(self.toDict()) 291 | 292 | def serialize(self) -> str: 293 | return self.origin if self.mod == 0 and self.origin is not None else self.concat() 294 | 295 | class HttpHeaders: 296 | 297 | def __init__(self, entries = None): 298 | self._entries = ([] if entries is None else entries) 299 | 300 | @classmethod 301 | def of(cls, data): 302 | if isinstance(data, list) and all(isinstance(e, str) for e in data): 303 | return cls(data) 304 | elif isinstance(data, list) and all(isinstance(t, tuple) and isinstance(t[0], str) and 305 | isinstance(t[1], str) for t in data): 306 | entries = [] 307 | for t in data: 308 | entries.append(f'{t[0]}: {t[1]}') 309 | return cls(entries) 310 | elif isinstance(data, dict) and all(isinstance(key, str) and isinstance(value, str) 311 | for key, value in data.items()): 312 | entries = [] 313 | for key, value in data.items(): 314 | entries.append(f'{key}: {value}') 315 | return cls(entries) 316 | raise Exception('Unsupported headers data type') 317 | 318 | def __len__(self): 319 | return len(self._entries) 320 | 321 | def __iter__(self): 322 | return iter(self._entries) 323 | 324 | def __str__(self): 325 | return str(self._entries) 326 | 327 | def __add__(self, other): 328 | return str(self) + other 329 | 330 | def __radd__(self, other): 331 | return other + str(self) 332 | 333 | def __getitem__(self, name) -> Union[str, None]: 334 | if isinstance(name, int): 335 | return self._entries[name] 336 | index = self.index(name) 337 | if index >= 0: 338 | return self._entries[index][len(name) + 2:] 339 | return None 340 | 341 | def __setitem__(self, name: str, value: str): 342 | if len(name) >= 1: 343 | index = self.index(name) 344 | if index >= 0: 345 | self._entries[index] = name + ': ' + value 346 | else: 347 | self._entries.append(name + ': ' + value) 348 | 349 | # Add a header line with name and value. 350 | def add(self, name: str, value: str): 351 | if not name: 352 | return 353 | self._entries.append(name + ': ' + value) 354 | 355 | # Remove headers by name, all the matched headers will be removed. 356 | def remove(self, name: str): 357 | if isinstance(name, str): 358 | for index in reversed(self.indexes(name)): 359 | self._entries.pop(index) 360 | 361 | # Find the first header index by name. If no matched, returns -1. 362 | def index(self, name: str) -> int: 363 | for i in range(len(self._entries)): 364 | if self._entries[i].lower().startswith(name.lower() + ': '): 365 | return i 366 | return -1 367 | 368 | # Find header indexes by name. 369 | def indexes(self, name: str) -> List[int]: 370 | indexes = [] 371 | for i in range(len(self._entries)): 372 | if self._entries[i].lower().startswith(name.lower() + ': '): 373 | indexes.append(i) 374 | return indexes 375 | 376 | # Remove all headers. 377 | def clear(self): 378 | self._entries.clear() 379 | 380 | # Get all header lines. 381 | @property 382 | def entries(self) -> List[str]: 383 | return self._entries 384 | 385 | # Convert the headers to a dict. 386 | def toDict(self) -> Dict[str, str]: 387 | map = {} 388 | for entry in self.entries: 389 | name, value = entry.split(': ') 390 | map[name] = value 391 | return map 392 | 393 | # Convert the headers to a json str. 394 | def toJson(self) -> str: 395 | return json.dumps(self.toDict()) 396 | 397 | def serialize(self) -> List[str]: 398 | return self._entries 399 | 400 | class HttpBody: 401 | 402 | __type_none = 0 403 | __type_text = 1 404 | __type_binary = 2 405 | __type_multipart = 3 406 | 407 | def __init__(self, type: int = 0, payload = None, charset = None): 408 | self._type = type 409 | self._payload = payload 410 | self._charset = charset 411 | 412 | @classmethod 413 | def of(cls, data = None): 414 | if isinstance(data, str): 415 | type = cls.__type_text 416 | payload = data 417 | elif isinstance(data, dict): 418 | type = cls.__type_text 419 | payload = json.dumps(data) 420 | elif isinstance(data, bytes): 421 | type = cls.__type_binary 422 | payload = data 423 | elif isinstance(data, HttpBody): 424 | return data 425 | else: 426 | type = cls.__type_none 427 | payload = None 428 | return cls(type, payload, 'UTF-8') 429 | 430 | @classmethod 431 | def parse(cls, dict): 432 | if dict == None: 433 | return cls(cls.__type_none, None) 434 | type = dict['type'] 435 | if type == cls.__type_none: 436 | payload = None 437 | charset = None 438 | elif type == cls.__type_text: 439 | payload = dict['payload']['text'] 440 | charset = dict['payload']['charset'] 441 | elif type == cls.__type_binary: 442 | payload = dict['payload'] 443 | charset = None 444 | if isinstance(payload, str): 445 | with open(payload, mode = 'rb') as file: 446 | payload = file.read() 447 | elif isinstance(payload, bytes): 448 | payload = payload 449 | else: 450 | payload = bytes() 451 | elif type == cls.__type_multipart: 452 | payload = [] 453 | charset = None 454 | for multipart in dict['payload']: 455 | payload.append(HttpMultipartBody(multipart)) 456 | return cls(type, payload, charset) 457 | 458 | def __repr__(self): 459 | if self.isMultipart: 460 | return f'Multipart {len(self._payload)} body' 461 | else: 462 | return str(self) 463 | 464 | def __add__(self, other): 465 | return str(self) + other 466 | 467 | def __radd__(self, other): 468 | return other + str(self) 469 | 470 | def __len__(self): 471 | return 0 if self.isNone else len(self._payload) 472 | 473 | def __iter__(self): 474 | return iter(self._payload) 475 | 476 | def __str__(self): 477 | if self.isNone: 478 | return '' 479 | elif self.isText: 480 | if isinstance(self._payload, str): 481 | return self._payload 482 | else: 483 | return json.dumps(self._payload) 484 | elif self.isBinary: 485 | return str(self._payload) 486 | else: 487 | raise Exception('Unsupported str for multipart body') 488 | 489 | # Deprecated! Use isNone/isText/isBinary/isMultipart instead. 490 | # Return the body type. 491 | @property 492 | def type(self) -> int: 493 | return self._type 494 | 495 | # The http body payload. 496 | @property 497 | def payload(self) -> Union[None, str, bytes, List]: 498 | return self._payload 499 | 500 | # Determine whether the body is None. 501 | @property 502 | def isNone(self) -> bool: 503 | return self._type is HttpBody.__type_none 504 | 505 | # Determine whether the body is a text string. 506 | @property 507 | def isText(self) -> bool: 508 | return self._type is HttpBody.__type_text 509 | 510 | # Determine whether the body is a binary bytes. 511 | @property 512 | def isBinary(self) -> bool: 513 | return self._type is HttpBody.__type_binary 514 | 515 | # Determine whether the body is a multipart type. 516 | @property 517 | def isMultipart(self) -> bool: 518 | return self._type is HttpBody.__type_multipart 519 | 520 | # Set the body to None. 521 | def none(self): 522 | self._type = HttpBody.__type_none 523 | self._payload = None 524 | 525 | # Set the body to a text string. 526 | def text(self, value: str): 527 | self._type = HttpBody.__type_text 528 | self._payload = value 529 | 530 | # Set the body to a specified file content, the file content must be a text string. 531 | def textFromFile(self, value: str): 532 | with open(value, mode = 'r', encoding='UTF-8') as file: 533 | self._payload = file.read() 534 | self._type = HttpBody.__type_text 535 | 536 | # Set the body to the specified file content, the file content must be a binary bytes. 537 | def file(self, value: str): 538 | if isinstance(value, str): 539 | self.binary(value) 540 | 541 | # Set the body to binary bytes. 542 | def binary(self, value: Union[str, bytes]): 543 | if isinstance(value, str): 544 | self._type = HttpBody.__type_binary 545 | with open(value, mode = 'rb') as file: 546 | self._payload = file.read() 547 | if isinstance(value, bytes): 548 | self._type = HttpBody.__type_binary 549 | self._payload = value 550 | 551 | # Set the body to binary bytes. 552 | def multiparts(self, value: list): 553 | if not isinstance(value, list): 554 | return 555 | payload = [] 556 | for multipart in value: 557 | if isinstance(multipart, HttpMultipartBody): 558 | payload.append(multipart) 559 | if len(payload) == 0: 560 | return 561 | self._type = HttpBody.__type_multipart 562 | self._payload = payload 563 | 564 | # Convert the body content to a json dict. 565 | def jsonify(self): 566 | if self.isText: 567 | self._payload = json.loads(self._payload) 568 | 569 | # Replace old string to a new one. The body type must be a text. 570 | def replace(self, old: str, new: str, count: int = -1): 571 | if self.isText and isinstance(self._payload, str): 572 | self._payload = self._payload.replace(old, new, count) 573 | 574 | # If the body type is a json dict, returns the value. Note: you must call jsonify() before this. 575 | # If the body type is binary, returns the value at the index. 576 | # If the body type is multipart, returns the part at the index. 577 | def __getitem__(self, name: Union[str, int]): 578 | if self.isText: 579 | if not isinstance(self._payload, dict): 580 | raise Exception('Did you forget to call `jsonify()` before operating json dict?') 581 | return self._payload[name] 582 | if self.isBinary and isinstance(name, int): 583 | return self._payload[name] 584 | if self.isMultipart and isinstance(name, int): 585 | return self._payload[name] 586 | return None 587 | 588 | # If the body type is a json dict, set the value. Note: you must call jsonify() before this. 589 | # If the body type is binary, set the value at the index. 590 | # If the body type is multipart, set the part at the index. 591 | def __setitem__(self, name: Union[str, int], value): 592 | if self.isText: 593 | if not isinstance(self._payload, dict): 594 | raise Exception('Did you forget to call `jsonify()` before operating json dict?') 595 | self._payload[name] = value 596 | if self.isBinary and isinstance(name, int): 597 | self._payload[name] = value 598 | if self.isMultipart and isinstance(name, int): 599 | self._payload[name] = value 600 | 601 | # Write the body content to a file. 602 | def writeFile(self, path: str): 603 | if self.isText: 604 | with open(path, "w", encoding='UTF-8') as file: 605 | if isinstance(self._payload, str): 606 | file.write(self._payload) 607 | else: 608 | file.write(json.dumps(self._payload)) 609 | elif self.isBinary: 610 | with open(path, "wb") as file: 611 | file.write(self._payload) 612 | elif self.isMultipart: 613 | raise Exception('Write a multipart body to file is supported!') 614 | 615 | def serialize(self): 616 | type = self._type 617 | if self.isNone: 618 | payload = None 619 | elif self.isText: 620 | if isinstance(self._payload, str): 621 | if len(self._payload) == 0: 622 | payload = None 623 | type = HttpBody.__type_none 624 | else: 625 | payload = { 626 | 'text': self._payload, 627 | 'charset': self._charset 628 | } 629 | else: 630 | payload = { 631 | 'text': json.dumps(self._payload), 632 | 'charset': self._charset 633 | } 634 | elif self.isBinary: 635 | if len(self._payload) == 0: 636 | type = HttpBody.__type_none 637 | payload = None 638 | else: 639 | payload = os.path.join(os.getcwd(), 'tmp-' + str(uuid.uuid4())) 640 | with open(payload, 'wb') as file: 641 | file.write(self._payload) 642 | elif self.isMultipart: 643 | if len(self._payload) == 0: 644 | type = HttpBody.__type_none 645 | payload = None 646 | else: 647 | payload = [] 648 | for multipart in self._payload: 649 | payload.append(multipart.serialize()) 650 | return { 651 | 'type': type, 652 | 'payload': payload, 653 | } 654 | 655 | class HttpMultipartBody(HttpBody): 656 | 657 | def __init__(self, json: dict): 658 | self._headers = HttpHeaders(json['headers']) 659 | body = HttpBody.parse(json['body']) 660 | super().__init__(body.type, body.payload) 661 | 662 | def _concatDisposition(name: str, filename: str, type: str): 663 | if name != '' and filename != '': 664 | return f'{type}; name="{name}"; filename="{filename}"' 665 | elif name != '': 666 | return f'{type}; name="{name}"' 667 | elif filename != '': 668 | return f'{type}; filename="{filename}"' 669 | return None 670 | 671 | @classmethod 672 | def text(cls, text: str, name: str = '', filename: str = '', headers = None, type = 'form-data', charset= 'UTF-8'): 673 | if headers is None: 674 | headers = [] 675 | headers.append(f'content-length: {len(text)}') 676 | disposition = HttpMultipartBody._concatDisposition(name, filename, type) 677 | if disposition is not None: 678 | headers.append(f'content-disposition: {disposition}') 679 | return cls({ 680 | 'headers': headers, 681 | 'body': { 682 | 'type': 1, 683 | 'payload': { 684 | 'text': text, 685 | 'charset': charset 686 | } 687 | } 688 | }) 689 | 690 | @classmethod 691 | def file(cls, file: str, name: str = '', filename: str = '', headers = None, type = 'form-data'): 692 | if headers is None: 693 | headers = [] 694 | headers.append(f'content-length: {os.stat(file).st_size}') 695 | disposition = HttpMultipartBody._concatDisposition(name, filename, type) 696 | if disposition is not None: 697 | headers.append(f'content-disposition: {disposition}') 698 | return cls({ 699 | 'headers': headers, 700 | 'body': { 701 | 'type': 2, 702 | 'payload': file 703 | } 704 | }) 705 | 706 | # Get the part headers. 707 | @property 708 | def headers(self) -> HttpHeaders: 709 | return self._headers 710 | 711 | # Set the part headers. 712 | @headers.setter 713 | def headers(self, data: Union[List[str], List[Tuple[str, str]], Dict[str, str]]): 714 | self._headers = HttpHeaders.of(data) 715 | 716 | # Get the part name. 717 | @property 718 | def name(self) -> Union[str, None]: 719 | return self._getDispositionParamValue('name') 720 | 721 | # Set the part name. 722 | @name.setter 723 | def name(self, data: str): 724 | self._setDispositionParamValue('name', data) 725 | 726 | # Get the part filename. 727 | @property 728 | def filename(self) -> Union[str, None]: 729 | return self._getDispositionParamValue('filename') 730 | 731 | # Set the part filename. 732 | @filename.setter 733 | def filename(self, data: str): 734 | self._setDispositionParamValue('filename', data) 735 | 736 | def serialize(self) -> dict: 737 | return { 738 | 'headers': self._headers.serialize(), 739 | 'body': super().serialize() 740 | } 741 | 742 | def _getDispositionParamValue(self, param): 743 | disposition = self._headers['content-disposition'] 744 | if disposition is None: 745 | return None 746 | message = EmailMessage() 747 | message.add_header('content-disposition', disposition) 748 | return message.get_param(param, header='content-disposition') 749 | 750 | def _setDispositionParamValue(self, param, value): 751 | disposition = self._headers['content-disposition'] 752 | if disposition is None: 753 | return 754 | message = EmailMessage() 755 | message.add_header('content-disposition', disposition) 756 | message.set_param(param, value, header='content-disposition') 757 | self._headers['content-disposition'] = message.get('content-disposition') 758 | 759 | class HttpRequest: 760 | def __init__(self, json): 761 | self._method = json['method'] 762 | self._protocol = json['protocol'] 763 | self._headers = HttpHeaders(json.get('headers')) 764 | self._body = HttpBody.parse(json.get('body')) 765 | self._trailers = HttpHeaders(json.get('trailers')) 766 | from urllib.parse import urlparse 767 | url = urlparse('https://reqable.com' + json['path']) 768 | self._params = url.params 769 | self._path = url.path 770 | self._queries = HttpQueries.parse(url.query) 771 | 772 | def __str__(self): 773 | return self.toJson() 774 | 775 | def __add__(self, other): 776 | return str(self) + other 777 | 778 | def __radd__(self, other): 779 | return other + str(self) 780 | 781 | # Get the request http protocol, such as HTTP/1.1, h2. 782 | @property 783 | def protocol(self) -> str: 784 | return self._protocol 785 | 786 | # Get the request http method, such as GET, POST. 787 | @property 788 | def method(self) -> str: 789 | return self._method 790 | 791 | # Set the request http method. 792 | @method.setter 793 | def method(self, data) -> str: 794 | if isinstance(data, str) and data != '': 795 | self._method = data 796 | else: 797 | raise Exception('Request method must be a non-empty string.') 798 | 799 | # Get the request path. 800 | @property 801 | def path(self) -> str: 802 | return self._path 803 | 804 | # Set the request path. 805 | @path.setter 806 | def path(self, data: str): 807 | if isinstance(data, str) and data != '': 808 | self._path = data 809 | else: 810 | raise Exception('Request path must be a non-empty string.') 811 | 812 | # Get the request query paramaters. 813 | @property 814 | def queries(self) -> HttpQueries: 815 | return self._queries 816 | 817 | # Set the request query paramaters. 818 | @queries.setter 819 | def queries(self, data: Union[str, List[Tuple[str, str]], Dict[str, str]]): 820 | self._queries = HttpQueries.of(data) 821 | 822 | # Get the request headers. 823 | @property 824 | def headers(self) -> HttpHeaders: 825 | return self._headers 826 | 827 | # Set the request headers. 828 | @headers.setter 829 | def headers(self, data: Union[List[str], List[Tuple[str, str]], Dict[str, str]]): 830 | self._headers = HttpHeaders.of(data) 831 | 832 | # Get the request trailers. Note that the implementation of this function is incomplete, please do not use it. 833 | @property 834 | def trailers(self) -> HttpHeaders: 835 | return self._trailers 836 | 837 | # Set the request trailers. Note that the implementation of this function is incomplete, please do not use it. 838 | @trailers.setter 839 | def trailers(self, data: Union[List[str], List[Tuple[str, str]], Dict[str, str]]): 840 | self._trailers = HttpHeaders.of(data) 841 | 842 | # Get the request body. 843 | @property 844 | def body(self) -> HttpBody: 845 | return self._body 846 | 847 | # Set the request body. 848 | @body.setter 849 | def body(self, data: Union[str, bytes, dict, HttpBody]): 850 | self._body = HttpBody.of(data) 851 | 852 | # Get the request content type from headers. 853 | @property 854 | def contentType(self) -> Union[str, None]: 855 | return self._headers['content-type'] 856 | 857 | # Set the request content type to headers. 858 | @contentType.setter 859 | def contentType(self, value: str): 860 | self._headers['content-type'] = value 861 | 862 | # Get the request mime type from headers. 863 | @property 864 | def mime(self) -> Union[str, None]: 865 | contentType = self._headers['content-type'] 866 | if contentType == None: 867 | return None 868 | message = EmailMessage() 869 | message.add_header('content-type', contentType) 870 | return message.get_content_type() 871 | 872 | # Serialize the request fields to a dict. 873 | def serialize(self) -> dict: 874 | path = self.path 875 | if self._params != None and self._params != '': 876 | path = path + ';' + self._params 877 | if len(self.queries) != 0: 878 | path = path + '?' + self.queries.serialize() 879 | return { 880 | 'method': self.method, 881 | 'path': path, 882 | 'protocol': self._protocol, 883 | 'headers': self._headers.serialize(), 884 | 'body': self._body.serialize(), 885 | 'trailers': self._trailers.serialize(), 886 | } 887 | 888 | def toJson(self) -> str: 889 | return json.dumps(self.serialize()) 890 | 891 | class HttpResponse: 892 | def __init__(self, json): 893 | self._request = HttpRequest(json['request']) 894 | self._code = json['code'] 895 | self._message = json['message'] 896 | self._protocol = json['protocol'] 897 | self._headers = HttpHeaders(json.get('headers')) 898 | self._body = HttpBody.parse(json.get('body')) 899 | self._trailers = HttpHeaders(json.get('trailers')) 900 | 901 | def __str__(self): 902 | return self.toJson() 903 | 904 | def __add__(self, other): 905 | return str(self) + other 906 | 907 | def __radd__(self, other): 908 | return other + str(self) 909 | 910 | # Get the request informations. 911 | @property 912 | def request(self) -> HttpRequest: 913 | return self._request 914 | 915 | # Get the response status code. 916 | @property 917 | def code(self) -> int: 918 | return self._code 919 | 920 | # Set the response status code. 921 | @code.setter 922 | def code(self, data: int): 923 | if isinstance(data, int) and data >= 100 and data <= 600: 924 | self._code = data 925 | else: 926 | raise Exception('Response code must be a int (100 - 600).') 927 | 928 | # Get the response status message, maybe None. 929 | @property 930 | def message(self) -> Union[str, None]: 931 | return self._message 932 | 933 | # Get the response http protocol, such as HTTP/1.1, h2. 934 | @property 935 | def protocol(self) -> str: 936 | return self._protocol 937 | 938 | # Get the response headers. 939 | @property 940 | def headers(self) -> HttpHeaders: 941 | return self._headers 942 | 943 | # Set the response headers. 944 | @headers.setter 945 | def headers(self, data: Union[List[str], List[Tuple[str, str]], Dict[str, str]]): 946 | self._headers = HttpHeaders.of(data) 947 | 948 | # Set the response trailers. Note that the implementation of this function is incomplete, please do not use it. 949 | @property 950 | def trailers(self) -> HttpHeaders: 951 | return self._trailers 952 | 953 | # Get the response trailers. Note that the implementation of this function is incomplete, please do not use it. 954 | @trailers.setter 955 | def trailers(self, data: Union[List[str], List[Tuple[str, str]], Dict[str, str]]): 956 | self._trailers = HttpHeaders.of(data) 957 | 958 | # Get the response body. 959 | @property 960 | def body(self) -> HttpBody: 961 | return self._body 962 | 963 | # Set the response body. 964 | @body.setter 965 | def body(self, data: Union[str, bytes, dict, HttpBody]): 966 | self._body = HttpBody.of(data) 967 | 968 | # Get the response content type from headers. 969 | @property 970 | def contentType(self) -> Union[str, None]: 971 | return self._headers['content-type'] 972 | 973 | # Set the response content type to headers. 974 | @contentType.setter 975 | def contentType(self, value: str): 976 | self._headers['content-type'] = value 977 | 978 | # Get the response mime type from headers. 979 | @property 980 | def mime(self) -> Union[str, None]: 981 | contentType = self._headers['content-type'] 982 | if contentType == None: 983 | return None 984 | message = EmailMessage() 985 | message.add_header('content-type', contentType) 986 | return message.get_content_type() 987 | 988 | # Serialize the response fields to a dict. 989 | def serialize(self) -> dict: 990 | return { 991 | 'request': self._request.serialize(), 992 | 'code': self.code, 993 | 'message': self._message, 994 | 'protocol': self._protocol, 995 | 'headers': self._headers.serialize(), 996 | 'body': self._body.serialize(), 997 | 'trailers': self._trailers.serialize(), 998 | } 999 | 1000 | def toJson(self) -> str: 1001 | return json.dumps(self.serialize()) 1002 | 1003 | #################################################################################################### 1004 | # Below is the legacy classes, they are deprecated and will be removed in the future. 1005 | #################################################################################################### 1006 | CaptureApp = App 1007 | CaptureContext = Context 1008 | CaptureHttpQueries = HttpQueries 1009 | CaptureHttpHeaders = HttpHeaders 1010 | CaptureHttpBody = HttpBody 1011 | CaptureHttpMultipartBody = HttpMultipartBody 1012 | CaptureHttpRequest = HttpRequest 1013 | CaptureHttpResponse = HttpResponse --------------------------------------------------------------------------------