stoobly-agent 0.34.10__py3-none-any.whl → 0.34.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/api/proxy_controller.py +12 -10
  3. stoobly_agent/app/cli/config_cli.py +5 -3
  4. stoobly_agent/app/cli/snapshot_cli.py +85 -6
  5. stoobly_agent/app/models/factories/resource/local_db/helpers/log.py +9 -7
  6. stoobly_agent/app/models/factories/resource/local_db/helpers/request_snapshot.py +5 -0
  7. stoobly_agent/app/models/factories/resource/local_db/helpers/scenario_snapshot.py +24 -0
  8. stoobly_agent/app/models/factories/resource/local_db/helpers/snapshot.py +18 -2
  9. stoobly_agent/app/models/factories/resource/local_db/query_param_adapter.py +9 -5
  10. stoobly_agent/app/proxy/handle_mock_service.py +15 -13
  11. stoobly_agent/app/proxy/handle_record_service.py +2 -2
  12. stoobly_agent/app/proxy/intercept_handler.py +7 -4
  13. stoobly_agent/app/proxy/mitmproxy/request_facade.py +4 -2
  14. stoobly_agent/app/proxy/mock/eval_fixtures_service.py +3 -1
  15. stoobly_agent/app/proxy/mock/hashed_request_decorator.py +10 -10
  16. stoobly_agent/app/proxy/mock/request_hasher.py +3 -1
  17. stoobly_agent/app/proxy/record/upload_request_service.py +5 -5
  18. stoobly_agent/app/proxy/replay/alias_resolver.py +5 -3
  19. stoobly_agent/app/proxy/replay/body_parser_service.py +1 -5
  20. stoobly_agent/app/proxy/replay/multipart.py +9 -27
  21. stoobly_agent/app/proxy/replay/trace_context.py +10 -9
  22. stoobly_agent/app/proxy/test/helpers/upload_test_service.py +5 -3
  23. stoobly_agent/app/proxy/utils/allowed_request_service.py +7 -5
  24. stoobly_agent/app/proxy/utils/request_handler.py +3 -1
  25. stoobly_agent/app/settings/__init__.py +32 -14
  26. stoobly_agent/cli.py +1 -1
  27. stoobly_agent/config/data_dir.py +39 -21
  28. stoobly_agent/lib/api/body_param_names_resource.py +5 -3
  29. stoobly_agent/lib/api/endpoints_resource.py +5 -3
  30. stoobly_agent/lib/api/header_names_resource.py +5 -3
  31. stoobly_agent/lib/api/projects_resource.py +5 -3
  32. stoobly_agent/lib/api/query_param_names_resource.py +5 -3
  33. stoobly_agent/lib/api/requests_resource.py +5 -3
  34. stoobly_agent/lib/api/response_header_names_resource.py +5 -3
  35. stoobly_agent/lib/api/response_param_names_resource.py +5 -3
  36. stoobly_agent/lib/api/scenarios_resource.py +5 -3
  37. stoobly_agent/lib/api/stoobly_api.py +0 -1
  38. stoobly_agent/lib/api/test_responses_resource.py +3 -1
  39. stoobly_agent/lib/api/tests_resource.py +3 -1
  40. stoobly_agent/lib/api/users_resource.py +3 -1
  41. stoobly_agent/lib/logger.py +5 -2
  42. stoobly_agent/lib/utils/visitor.py +4 -3
  43. stoobly_agent/test/app/cli/snapshot/snapshot_copy_test.py +56 -0
  44. stoobly_agent/test/app/cli/snapshot/snapshot_prune_test.py +2 -5
  45. stoobly_agent/test/app/cli/snapshot/snapshot_update_test.py +0 -1
  46. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  47. stoobly_agent/test/app/proxy/replay/body_parser_service_test.py +3 -3
  48. stoobly_agent/test/config/data_dir_test.py +4 -2
  49. {stoobly_agent-0.34.10.dist-info → stoobly_agent-0.34.12.dist-info}/METADATA +2 -1
  50. {stoobly_agent-0.34.10.dist-info → stoobly_agent-0.34.12.dist-info}/RECORD +53 -52
  51. {stoobly_agent-0.34.10.dist-info → stoobly_agent-0.34.12.dist-info}/LICENSE +0 -0
  52. {stoobly_agent-0.34.10.dist-info → stoobly_agent-0.34.12.dist-info}/WHEEL +0 -0
  53. {stoobly_agent-0.34.10.dist-info → stoobly_agent-0.34.12.dist-info}/entry_points.txt +0 -0
@@ -5,6 +5,8 @@ from ..logger import Logger
5
5
  from .interfaces import RequestCreateParams, RequestsIndexQueryParams, RequestShowQueryParams
6
6
  from .stoobly_api import StooblyApi
7
7
 
8
+ LOG_ID = 'RequestsResource'
9
+
8
10
  class RequestsResource(StooblyApi):
9
11
 
10
12
  def create(self, **body_params: RequestCreateParams):
@@ -22,14 +24,14 @@ class RequestsResource(StooblyApi):
22
24
  def index(self, **query_params: RequestsIndexQueryParams):
23
25
  url = f"{self.service_url}{self.REQUESTS_ENDPOINT}"
24
26
 
25
- Logger.instance().debug(f"{self.LOG_ID}.request_response:{url}?{urllib.parse.urlencode(query_params)}")
27
+ Logger.instance(LOG_ID).debug(f"{url}?{urllib.parse.urlencode(query_params)}")
26
28
 
27
29
  return self.get(url, headers=self.default_headers, params=query_params)
28
30
 
29
31
  def response(self, **query_params):
30
32
  url = f"{self.service_url}{self.REQUESTS_ENDPOINT}/response"
31
33
 
32
- Logger.instance().debug(f"{self.LOG_ID}.request_response:{url}?{urllib.parse.urlencode(query_params)}")
34
+ Logger.instance(LOG_ID).debug(f"{url}?{urllib.parse.urlencode(query_params)}")
33
35
 
34
36
  return self.get(
35
37
  url,
@@ -42,7 +44,7 @@ class RequestsResource(StooblyApi):
42
44
  def show(self, request_id, **query_params: RequestShowQueryParams):
43
45
  url = f"{self.service_url}{self.REQUESTS_ENDPOINT}/{request_id}"
44
46
 
45
- Logger.instance().debug(f"{self.LOG_ID}.request_response:{url}?{urllib.parse.urlencode(query_params)}")
47
+ Logger.instance(LOG_ID).debug(f"{url}?{urllib.parse.urlencode(query_params)}")
46
48
 
47
49
  return self.get(url, headers=self.default_headers, params=query_params)
48
50
 
@@ -7,6 +7,8 @@ from stoobly_agent.app.models.types import HeaderNameCreateParams
7
7
  from ..logger import Logger
8
8
  from .endpoints_resource import EndpointsResource
9
9
 
10
+ LOG_ID = 'ResponseHeaderNamesResource'
11
+
10
12
  class ResponseHeaderNamesResource(EndpointsResource):
11
13
  RESPONSE_HEADER_NAME_ENDPOINT = 'response_header_names'
12
14
 
@@ -17,20 +19,20 @@ class ResponseHeaderNamesResource(EndpointsResource):
17
19
  def index(self, endpoint_id: int, query_params = {}) -> requests.Response:
18
20
  url = f"{self.service_url}/{self.ENDPOINTS_ENDPOINT}/{endpoint_id}/{self.RESPONSE_HEADER_NAME_ENDPOINT}"
19
21
 
20
- Logger.instance().debug(f"{self.LOG_ID}.request_response:{url}?{urllib.parse.urlencode(query_params)}")
22
+ Logger.instance(LOG_ID).debug(f"{url}?{urllib.parse.urlencode(query_params)}")
21
23
 
22
24
  return self.get(url, headers=self.default_headers, params=query_params)
23
25
 
24
26
  def show(self, endpoint_id: int, response_header_name_id: int, query_params = {}) -> requests.Response:
25
27
  url = f"{self.service_url}/{self.ENDPOINTS_ENDPOINT}/{endpoint_id}/{self.RESPONSE_HEADER_NAME_ENDPOINT}/{response_header_name_id}"
26
28
 
27
- Logger.instance().debug(f"{self.LOG_ID}.request_response:{url}?{urllib.parse.urlencode(query_params)}")
29
+ Logger.instance(LOG_ID).debug(f"{url}?{urllib.parse.urlencode(query_params)}")
28
30
 
29
31
  return self.get(url, headers=self.default_headers, params=query_params)
30
32
 
31
33
  def destroy(self, endpoint_id: int, response_header_name_id: int, query_params = {}) -> requests.Response:
32
34
  url = f"{self.service_url}/{self.ENDPOINTS_ENDPOINT}/{endpoint_id}/{self.RESPONSE_HEADER_NAME_ENDPOINT}/{response_header_name_id}"
33
35
 
34
- Logger.instance().debug(f"{self.LOG_ID}.request_response:{url}?{urllib.parse.urlencode(query_params)}")
36
+ Logger.instance(LOG_ID).debug(f"{url}?{urllib.parse.urlencode(query_params)}")
35
37
 
36
38
  return self.delete(url, headers=self.default_headers, params=query_params)
@@ -7,6 +7,8 @@ from stoobly_agent.app.models.types import ParamNameCreateParams
7
7
  from ..logger import Logger
8
8
  from .endpoints_resource import EndpointsResource
9
9
 
10
+ LOG_ID = 'ResponseParamNamesResource'
11
+
10
12
  class ResponseParamNamesResource(EndpointsResource):
11
13
  RESPONSE_PARAM_NAMES_ENDPOINT = 'response_param_names'
12
14
 
@@ -17,20 +19,20 @@ class ResponseParamNamesResource(EndpointsResource):
17
19
  def index(self, endpoint_id: int, query_params = {}) -> requests.Response:
18
20
  url = f"{self.service_url}/{self.ENDPOINTS_ENDPOINT}/{endpoint_id}/{self.RESPONSE_PARAM_NAMES_ENDPOINT}"
19
21
 
20
- Logger.instance().debug(f"{self.LOG_ID}.request_response:{url}?{urllib.parse.urlencode(query_params)}")
22
+ Logger.instance(LOG_ID).debug(f"{url}?{urllib.parse.urlencode(query_params)}")
21
23
 
22
24
  return self.get(url, headers=self.default_headers, params=query_params)
23
25
 
24
26
  def show(self, endpoint_id: int, response_param_name_id: int, query_params = {}) -> requests.Response:
25
27
  url = f"{self.service_url}/{self.ENDPOINTS_ENDPOINT}/{endpoint_id}/{self.RESPONSE_PARAM_NAMES_ENDPOINT}/{response_param_name_id}"
26
28
 
27
- Logger.instance().debug(f"{self.LOG_ID}.request_response:{url}?{urllib.parse.urlencode(query_params)}")
29
+ Logger.instance(LOG_ID).debug(f"{url}?{urllib.parse.urlencode(query_params)}")
28
30
 
29
31
  return self.get(url, headers=self.default_headers, params=query_params)
30
32
 
31
33
  def destroy(self, endpoint_id: int, response_param_name_id: int, query_params = {}) -> requests.Response:
32
34
  url = f"{self.service_url}/{self.ENDPOINTS_ENDPOINT}/{endpoint_id}/{self.RESPONSE_PARAM_NAMES_ENDPOINT}/{response_param_name_id}"
33
35
 
34
- Logger.instance().debug(f"{self.LOG_ID}.request_response:{url}?{urllib.parse.urlencode(query_params)}")
36
+ Logger.instance(LOG_ID).debug(f"{url}?{urllib.parse.urlencode(query_params)}")
35
37
 
36
38
  return self.delete(url, headers=self.default_headers, params=query_params)
@@ -7,6 +7,8 @@ from ..logger import Logger
7
7
  from .interfaces.pagination_query_params import PaginationQueryParams
8
8
  from .stoobly_api import StooblyApi
9
9
 
10
+ LOG_ID = 'ScenariosResource'
11
+
10
12
  class ScenariosResource(StooblyApi):
11
13
  SCENARIOS_ENDPOINT = 'scenarios'
12
14
 
@@ -17,20 +19,20 @@ class ScenariosResource(StooblyApi):
17
19
  def index(self, **query_params: PaginationQueryParams) -> requests.Response:
18
20
  url = f"{self.service_url}/{self.SCENARIOS_ENDPOINT}"
19
21
 
20
- Logger.instance().debug(f"{self.LOG_ID}.request_response:{url}?{urllib.parse.urlencode(query_params)}")
22
+ Logger.instance(LOG_ID).debug(f"{url}?{urllib.parse.urlencode(query_params)}")
21
23
 
22
24
  return self.get(url, headers=self.default_headers, params=query_params)
23
25
 
24
26
  def show(self, scenario_id: int, **query_params) -> requests.Response:
25
27
  url = f"{self.service_url}/{self.SCENARIOS_ENDPOINT}/{scenario_id}"
26
28
 
27
- Logger.instance().debug(f"{self.LOG_ID}.request_response:{url}?{urllib.parse.urlencode(query_params)}")
29
+ Logger.instance(LOG_ID).debug(f"{url}?{urllib.parse.urlencode(query_params)}")
28
30
 
29
31
  return self.get(url, headers=self.default_headers, params=query_params)
30
32
 
31
33
  def update(self, scenario_id: int, **params):
32
34
  url = f"{self.service_url}/{self.SCENARIOS_ENDPOINT}/{scenario_id}"
33
35
 
34
- Logger.instance().debug(f"{self.LOG_ID}.request_response:{url}")
36
+ Logger.instance(LOG_ID).debug(f"{url}")
35
37
 
36
38
  return self.put(url, headers=self.default_headers, json=params)
@@ -9,7 +9,6 @@ from stoobly_agent.lib.api.keys import InvalidReportKey, InvalidScenarioKey, Pro
9
9
  from .api import Api
10
10
 
11
11
  class StooblyApi(Api):
12
- LOG_ID = 'lib.api.stoobly_api'
13
12
  REPORTS_ENDPOINT = '/reports'
14
13
  REQUESTS_ENDPOINT = '/requests'
15
14
  TESTS_ENDPOINT = '/tests'
@@ -4,12 +4,14 @@ import urllib
4
4
  from ..logger import Logger
5
5
  from .tests_resource import TestsResource
6
6
 
7
+ LOG_ID = 'TestResponsesResource'
8
+
7
9
  class TestResponsesResource(TestsResource):
8
10
  RESPONSES_ENDPOINT = 'responses'
9
11
 
10
12
  def mock(self, test_id: int, **query_params) -> requests.Response:
11
13
  url = f"{self.service_url}/{self.TESTS_ENDPOINT}/{test_id}/{self.RESPONSES_ENDPOINT}/mock"
12
14
 
13
- Logger.instance().debug(f"{self.LOG_ID}.test_response_response:{url}?{urllib.parse.urlencode(query_params)}")
15
+ Logger.instance(LOG_ID).debug(f"{url}?{urllib.parse.urlencode(query_params)}")
14
16
 
15
17
  return self.get(url, headers=self.default_headers, params=query_params)
@@ -6,6 +6,8 @@ from stoobly_agent.lib.api.interfaces.tests import TestCreateParams
6
6
  from ..logger import Logger
7
7
  from .stoobly_api import StooblyApi
8
8
 
9
+ LOG_ID = 'TestsResource'
10
+
9
11
  class TestsResource(StooblyApi):
10
12
  TESTS_ENDPOINT = 'tests'
11
13
 
@@ -22,6 +24,6 @@ class TestsResource(StooblyApi):
22
24
  def show(self, test_id: int, **query_params) -> requests.Response:
23
25
  url = f"{self.service_url}/{self.TESTS_ENDPOINT}/{test_id}"
24
26
 
25
- Logger.instance().debug(f"{self.LOG_ID}.request_response:{url}?{urllib.parse.urlencode(query_params)}")
27
+ Logger.instance(LOG_ID).debug(f"{url}?{urllib.parse.urlencode(query_params)}")
26
28
 
27
29
  return self.get(url, headers=self.default_headers, params=query_params)
@@ -5,12 +5,14 @@ import pdb
5
5
  from ..logger import Logger
6
6
  from .stoobly_api import StooblyApi
7
7
 
8
+ LOG_ID = 'UsersResource'
9
+
8
10
  class UsersResource(StooblyApi):
9
11
  USERS_ENDPOINT = 'users'
10
12
 
11
13
  def profile(self, query_params = {}) -> requests.Response:
12
14
  url = f"{self.service_url}/{self.USERS_ENDPOINT}/profile"
13
15
 
14
- Logger.instance().debug(f"{self.LOG_ID}.request_response:{url}?{urllib.parse.urlencode(query_params)}")
16
+ Logger.instance(LOG_ID).debug(f"{url}?{urllib.parse.urlencode(query_params)}")
15
17
 
16
18
  return self.get(url, headers=self.default_headers, params=query_params)
@@ -31,13 +31,16 @@ class Logger:
31
31
  raise RuntimeError('Call instance() instead')
32
32
 
33
33
  @classmethod
34
- def instance(cls):
34
+ def instance(cls, name = None):
35
35
  if cls._instance is None:
36
36
  cls._instance = cls.__new__(cls)
37
37
 
38
38
  cls._instance.load()
39
39
 
40
- return logging
40
+ if not name:
41
+ return logging
42
+ else:
43
+ return logging.getLogger(name)
41
44
 
42
45
  @classmethod
43
46
  def reload(cls):
@@ -8,6 +8,7 @@ from jmespath.compat import string_type
8
8
  from numbers import Number
9
9
  from stoobly_agent.lib.logger import Logger
10
10
 
11
+ LOG_ID = 'JmesPath'
11
12
 
12
13
  def _equals(x, y):
13
14
  if _is_special_integer_case(x, y):
@@ -108,7 +109,7 @@ class Visitor(object):
108
109
 
109
110
  def increment_visits(self, node):
110
111
  self.visited_nodes.append(node)
111
- Logger.instance().debug(f"{len(self.visited_nodes)} {node}")
112
+ Logger.instance(LOG_ID).debug(f"{len(self.visited_nodes)} {node}")
112
113
 
113
114
  def build_traverse_path(self, node):
114
115
  if len(self.nodes) > 0:
@@ -118,9 +119,9 @@ class Visitor(object):
118
119
 
119
120
  self.build_traverse_path_traverse(node)
120
121
 
121
- Logger.instance().debug('Traverse Path')
122
+ Logger.instance(LOG_ID).debug('Traverse Path')
122
123
  for i, node in enumerate(self.nodes):
123
- Logger.instance().debug(f"{i + 1} {node}")
124
+ Logger.instance(LOG_ID).debug(f"{i + 1} {node}")
124
125
 
125
126
  def build_traverse_path_traverse(self, node):
126
127
  if node['type'] == 'projection':
@@ -0,0 +1,56 @@
1
+ import importlib
2
+ import os
3
+ import pdb
4
+ import pytest
5
+ import tempfile
6
+
7
+ from click.testing import CliRunner
8
+
9
+ from stoobly_agent.app.models.factories.resource.local_db.helpers.log import Log
10
+ from stoobly_agent.cli import record, request, snapshot
11
+ from stoobly_agent.config.data_dir import DataDir
12
+ from stoobly_agent.lib.api.keys import RequestKey
13
+ from stoobly_agent.lib.orm.request import Request
14
+ from stoobly_agent.test.test_helper import DETERMINISTIC_GET_REQUEST_URL, reset
15
+
16
+ @pytest.fixture(scope='module')
17
+ def runner():
18
+ return CliRunner()
19
+
20
+ class TestCopy():
21
+ @pytest.fixture(scope='class', autouse=True)
22
+ def settings(self):
23
+ return reset()
24
+
25
+ class TestWhenRequest():
26
+ @pytest.fixture(scope='class')
27
+ def recorded_request(self, runner: CliRunner):
28
+ record_result = runner.invoke(record, [DETERMINISTIC_GET_REQUEST_URL])
29
+ assert record_result.exit_code == 0
30
+ return Request.last()
31
+
32
+ @pytest.fixture(autouse=True, scope='class')
33
+ def snapshot_result(self, runner: CliRunner, recorded_request: Request):
34
+ snapshot_result = runner.invoke(request, ['snapshot', recorded_request.key()])
35
+ assert snapshot_result.exit_code == 0
36
+ return snapshot_result
37
+
38
+ def test_it_copies(self, runner: CliRunner, recorded_request: Request):
39
+ with tempfile.TemporaryDirectory() as dirpath:
40
+ data_dir = DataDir(dirpath)
41
+ log = Log(data_dir)
42
+
43
+ # Check that no events have been copied
44
+ assert len(log.events) == 0
45
+
46
+ apply_result = runner.invoke(snapshot, ['copy', dirpath, '--request-key', recorded_request.key()])
47
+ assert apply_result.exit_code == 0
48
+
49
+ # Check that one event has been copied
50
+ assert len(log.events) == 1
51
+
52
+ event = log.events[0]
53
+ request_key = RequestKey(recorded_request.key())
54
+
55
+ # Check that the event is the request
56
+ assert request_key.id == event.resource_uuid
@@ -3,15 +3,12 @@ import pytest
3
3
  import time
4
4
 
5
5
  from click.testing import CliRunner
6
- from typing import List
7
6
 
8
7
  from stoobly_agent.app.models.factories.resource.local_db.helpers.log import Log
9
8
  from stoobly_agent.app.models.factories.resource.local_db.helpers.log_event import DELETE_ACTION, LogEvent
10
- from stoobly_agent.app.models.factories.resource.local_db.helpers.scenario_snapshot import ScenarioSnapshot
11
- from stoobly_agent.cli import record, request, scenario, snapshot
9
+ from stoobly_agent.cli import record, request, snapshot
12
10
  from stoobly_agent.lib.orm.request import Request
13
- from stoobly_agent.lib.orm.scenario import Scenario
14
- from stoobly_agent.test.test_helper import assert_orm_request_equivalent, DETERMINISTIC_GET_REQUEST_URL, NON_DETERMINISTIC_GET_REQUEST_URL, reset
11
+ from stoobly_agent.test.test_helper import DETERMINISTIC_GET_REQUEST_URL, reset
15
12
 
16
13
  @pytest.fixture(scope='module')
17
14
  def runner():
@@ -3,7 +3,6 @@ import pytest
3
3
  import time
4
4
 
5
5
  from click.testing import CliRunner
6
- from typing import List
7
6
 
8
7
  from stoobly_agent.app.models.adapters.joined_request_adapter import JoinedRequestAdapter
9
8
  from stoobly_agent.app.models.adapters.raw_http_response_adapter import RawHttpResponseAdapter
@@ -1 +1 @@
1
- 0.34.8
1
+ 0.34.11
@@ -16,11 +16,11 @@ class TestMultipart():
16
16
  def test_decodes_response(self, multipart_string: bytes, content_type: str):
17
17
  multidict = decode_response(multipart_string, content_type)
18
18
 
19
- assert multidict.get('author') == b'John Smith'
20
- assert multidict.get('file') == b'Hello World'
19
+ assert multidict.get('author') == 'John Smith'
20
+ assert multidict.get('file') == 'Hello World'
21
21
 
22
22
  def test_encodes_response(self, content_type: str):
23
- expected_params = { 'author': b'John Smith', 'file': b'Hello World'}
23
+ expected_params = { 'author': 'John Smith', 'file': 'Hello World'}
24
24
  multipart_string = encode_response(expected_params, content_type)
25
25
 
26
26
  multidict = decode_response(multipart_string, content_type)
@@ -34,7 +34,8 @@ class TestDataDir():
34
34
 
35
35
  def test_in_cwd(self, original_cwd: str):
36
36
  os.environ[ENV] = NONE
37
- DataDir._instance = None
37
+ DataDir._instances = None
38
+
38
39
  temp_dir = os.path.join(original_cwd, 'tmp')
39
40
  nested_temp_dir = os.path.join(temp_dir, 'tmp-nested')
40
41
  data_dir_path = os.path.join(nested_temp_dir, DataDir.DATA_DIR_NAME)
@@ -55,7 +56,8 @@ class TestDataDir():
55
56
 
56
57
  def test_in_parent_nested(self, original_cwd: str):
57
58
  os.environ[ENV] = NONE
58
- DataDir._instance = None
59
+ DataDir._instances = None
60
+
59
61
  # Create a temporary directory structure for testing
60
62
  temp_dir = os.path.join(original_cwd, 'tmp')
61
63
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: stoobly-agent
3
- Version: 0.34.10
3
+ Version: 0.34.12
4
4
  Summary: Record, mock, and test HTTP(s) requests. CLI agent for Stoobly
5
5
  License: Apache-2.0
6
6
  Author: Matt Le
@@ -20,6 +20,7 @@ Requires-Dist: httptools (>=0.4.0)
20
20
  Requires-Dist: jmespath (>=1.0.0)
21
21
  Requires-Dist: mergedeep (>=1.3.0,<1.3.4)
22
22
  Requires-Dist: mitmproxy (>=8.0.0,<=8.1.0)
23
+ Requires-Dist: multipart (>=0.2.5,<0.3.0)
23
24
  Requires-Dist: openapi-core (>=0.17.0,<0.18.0)
24
25
  Requires-Dist: pyyaml (>=6.0.1)
25
26
  Requires-Dist: requests (>=2.31.0)