stoobly-agent 0.33.6__py3-none-any.whl → 0.34.0__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 (146) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/cli/endpoint_cli.py +24 -1
  3. stoobly_agent/app/cli/helpers/endpoint_facade.py +22 -1
  4. stoobly_agent/app/cli/helpers/endpoints_import_context.py +57 -0
  5. stoobly_agent/app/cli/helpers/endpoints_import_service.py +147 -0
  6. stoobly_agent/app/cli/helpers/openapi_endpoint_adapter.py +111 -6
  7. stoobly_agent/app/models/types/endpoint.py +28 -2
  8. stoobly_agent/app/proxy/mitmproxy/request_facade.py +1 -1
  9. stoobly_agent/app/settings/intercept_settings.py +1 -1
  10. stoobly_agent/lib/api/api.py +4 -0
  11. stoobly_agent/lib/api/body_param_names_resource.py +36 -0
  12. stoobly_agent/lib/api/endpoints_resource.py +9 -2
  13. stoobly_agent/lib/api/header_names_resource.py +36 -0
  14. stoobly_agent/lib/api/interfaces/endpoints.py +2 -0
  15. stoobly_agent/lib/api/query_param_names_resource.py +36 -0
  16. stoobly_agent/lib/api/response_header_names_resource.py +36 -0
  17. stoobly_agent/lib/api/response_param_names_resource.py +36 -0
  18. stoobly_agent/public/0-es2015.a7e60cafc0868f87a771.js +1 -0
  19. stoobly_agent/public/0-es5.a7e60cafc0868f87a771.js +1 -0
  20. stoobly_agent/public/1-es2015.cb071776436f10954db5.js +1 -0
  21. stoobly_agent/public/1-es5.cb071776436f10954db5.js +1 -0
  22. stoobly_agent/public/10-es2015.bb1702aaf3968a2cb521.js +1 -0
  23. stoobly_agent/public/10-es5.bb1702aaf3968a2cb521.js +1 -0
  24. stoobly_agent/public/12-es2015.591ec692afb8f8d13842.js +1 -0
  25. stoobly_agent/public/12-es5.591ec692afb8f8d13842.js +1 -0
  26. stoobly_agent/public/13-es2015.f381e7d6ff361b669e12.js +1 -0
  27. stoobly_agent/public/13-es5.f381e7d6ff361b669e12.js +1 -0
  28. stoobly_agent/public/14-es2015.1ffb225a16d6292dbddd.js +1 -0
  29. stoobly_agent/public/14-es5.1ffb225a16d6292dbddd.js +1 -0
  30. stoobly_agent/public/15-es2015.2cf39bcaeb0ea2c52297.js +1 -0
  31. stoobly_agent/public/15-es5.2cf39bcaeb0ea2c52297.js +1 -0
  32. stoobly_agent/public/16-es2015.0e9422b274e642f95e41.js +1 -0
  33. stoobly_agent/public/16-es5.0e9422b274e642f95e41.js +1 -0
  34. stoobly_agent/public/17-es2015.7fb08367a22d1e34aed7.js +1 -0
  35. stoobly_agent/public/17-es5.7fb08367a22d1e34aed7.js +1 -0
  36. stoobly_agent/public/18-es2015.5caa4789d1c335e628f2.js +1 -0
  37. stoobly_agent/public/18-es5.5caa4789d1c335e628f2.js +1 -0
  38. stoobly_agent/public/19-es2015.8049aef58c329c500f9b.js +1 -0
  39. stoobly_agent/public/19-es5.8049aef58c329c500f9b.js +1 -0
  40. stoobly_agent/public/2-es2015.8f184ac63348ba447b1f.js +1 -0
  41. stoobly_agent/public/2-es5.8f184ac63348ba447b1f.js +1 -0
  42. stoobly_agent/public/20-es2015.473486aabfa4d4a6431b.js +1 -0
  43. stoobly_agent/public/20-es5.473486aabfa4d4a6431b.js +1 -0
  44. stoobly_agent/public/21-es2015.63ed4e6b242fbc047bd6.js +1 -0
  45. stoobly_agent/public/21-es5.63ed4e6b242fbc047bd6.js +1 -0
  46. stoobly_agent/public/22-es2015.df183804415330639987.js +1 -0
  47. stoobly_agent/public/22-es5.df183804415330639987.js +1 -0
  48. stoobly_agent/public/23-es2015.0da63b056fabde91bb0b.js +1 -0
  49. stoobly_agent/public/23-es5.0da63b056fabde91bb0b.js +1 -0
  50. stoobly_agent/public/28-es2015.9fe030e9d3b0e52239aa.js +1 -0
  51. stoobly_agent/public/28-es5.9fe030e9d3b0e52239aa.js +1 -0
  52. stoobly_agent/public/29-es2015.9b440f22de725732e5ab.js +1 -0
  53. stoobly_agent/public/29-es5.9b440f22de725732e5ab.js +1 -0
  54. stoobly_agent/public/30-es2015.6ad2a5126b0a27c1e7c6.js +1 -0
  55. stoobly_agent/public/30-es5.6ad2a5126b0a27c1e7c6.js +1 -0
  56. stoobly_agent/public/31-es2015.f4291150f35d54ff19ca.js +1 -0
  57. stoobly_agent/public/31-es5.f4291150f35d54ff19ca.js +1 -0
  58. stoobly_agent/public/32-es2015.c8f6ccb43bdba0adf199.js +1 -0
  59. stoobly_agent/public/32-es5.c8f6ccb43bdba0adf199.js +1 -0
  60. stoobly_agent/public/33-es2015.f985e93402ebf86322ef.js +1 -0
  61. stoobly_agent/public/33-es5.f985e93402ebf86322ef.js +1 -0
  62. stoobly_agent/public/34-es2015.0e1961fb3cf649a52d49.js +1 -0
  63. stoobly_agent/public/34-es5.0e1961fb3cf649a52d49.js +1 -0
  64. stoobly_agent/public/35-es2015.a4ae56a89c0324397624.js +1 -0
  65. stoobly_agent/public/35-es5.a4ae56a89c0324397624.js +1 -0
  66. stoobly_agent/public/36-es2015.b8fdd25590a79c820119.js +1 -0
  67. stoobly_agent/public/36-es5.b8fdd25590a79c820119.js +1 -0
  68. stoobly_agent/public/37-es2015.6567a0ce4cf87ad7287b.js +1 -0
  69. stoobly_agent/public/37-es5.6567a0ce4cf87ad7287b.js +1 -0
  70. stoobly_agent/public/38-es2015.6f6d751bff41d84d727a.js +1 -0
  71. stoobly_agent/public/38-es5.6f6d751bff41d84d727a.js +1 -0
  72. stoobly_agent/public/39-es2015.47f63177e7d4509a22fa.js +1 -0
  73. stoobly_agent/public/39-es5.47f63177e7d4509a22fa.js +1 -0
  74. stoobly_agent/public/3rdpartylicenses.txt +2418 -0
  75. stoobly_agent/public/4-es2015.182e1ce1811ef67571fb.js +1 -0
  76. stoobly_agent/public/4-es5.182e1ce1811ef67571fb.js +1 -0
  77. stoobly_agent/public/40-es2015.5333067cdc4077c7495a.js +1 -0
  78. stoobly_agent/public/40-es5.5333067cdc4077c7495a.js +1 -0
  79. stoobly_agent/public/41-es2015.7a2434380c81c11ff2c5.js +1 -0
  80. stoobly_agent/public/41-es5.7a2434380c81c11ff2c5.js +1 -0
  81. stoobly_agent/public/42-es2015.5dde662fe1e3b4e4bdd1.js +1 -0
  82. stoobly_agent/public/42-es5.5dde662fe1e3b4e4bdd1.js +1 -0
  83. stoobly_agent/public/43-es2015.608e917d689b9bb762cb.js +1 -0
  84. stoobly_agent/public/43-es5.608e917d689b9bb762cb.js +1 -0
  85. stoobly_agent/public/44-es2015.2ae5fea15b5e8c2681d3.js +1 -0
  86. stoobly_agent/public/44-es5.2ae5fea15b5e8c2681d3.js +1 -0
  87. stoobly_agent/public/45-es2015.e46f228c795174428515.js +1 -0
  88. stoobly_agent/public/45-es5.e46f228c795174428515.js +1 -0
  89. stoobly_agent/public/46-es2015.22a0d8e0b4bbfb513741.js +1 -0
  90. stoobly_agent/public/46-es5.22a0d8e0b4bbfb513741.js +1 -0
  91. stoobly_agent/public/47-es2015.3878e5d1d692107748f3.js +1 -0
  92. stoobly_agent/public/47-es5.3878e5d1d692107748f3.js +1 -0
  93. stoobly_agent/public/5-es2015.aba7173be56fc19b4b6f.js +1 -0
  94. stoobly_agent/public/5-es5.aba7173be56fc19b4b6f.js +1 -0
  95. stoobly_agent/public/6-es2015.5fb726c0555664300974.js +1 -0
  96. stoobly_agent/public/6-es5.5fb726c0555664300974.js +1 -0
  97. stoobly_agent/public/7-es2015.9b9ab4adf24d13bdc2f8.js +1 -0
  98. stoobly_agent/public/7-es5.9b9ab4adf24d13bdc2f8.js +1 -0
  99. stoobly_agent/public/8-es2015.cdd7dce2a24aaf9d974f.js +1 -0
  100. stoobly_agent/public/8-es5.cdd7dce2a24aaf9d974f.js +1 -0
  101. stoobly_agent/public/9-es2015.cdde98f2537997afbf0f.js +1 -0
  102. stoobly_agent/public/9-es5.cdde98f2537997afbf0f.js +1 -0
  103. stoobly_agent/public/CHANGELOG.md +58 -0
  104. stoobly_agent/public/README.md +264 -0
  105. stoobly_agent/public/_redirects +1 -0
  106. stoobly_agent/public/assets/img/demo/1.jpg +0 -0
  107. stoobly_agent/public/assets/img/demo/2.jpg +0 -0
  108. stoobly_agent/public/assets/img/demo/3.jpg +0 -0
  109. stoobly_agent/public/assets/img/demo/4.jpg +0 -0
  110. stoobly_agent/public/assets/img/demo/5.jpg +0 -0
  111. stoobly_agent/public/assets/img/demo/6.jpg +0 -0
  112. stoobly_agent/public/assets/img/demo/7.jpg +0 -0
  113. stoobly_agent/public/assets/img/demo/8.jpg +0 -0
  114. stoobly_agent/public/assets/img/demo/landscape.jpg +0 -0
  115. stoobly_agent/public/assets/img/demo/mountain-cinematic.jpg +0 -0
  116. stoobly_agent/public/assets/img/illustrations/checklist.svg +164 -0
  117. stoobly_agent/public/assets/img/illustrations/data_center.svg +150 -0
  118. stoobly_agent/public/assets/img/illustrations/idea.svg +213 -0
  119. stoobly_agent/public/assets/img/illustrations/it_support.svg +168 -0
  120. stoobly_agent/public/assets/img/illustrations/peak_mountain_3.svg +262 -0
  121. stoobly_agent/public/assets/img/illustrations/under_constructions_1.svg +282 -0
  122. stoobly_agent/public/assets/img/logo/colored.png +0 -0
  123. stoobly_agent/public/assets/img/logo/colored.svg +9 -0
  124. stoobly_agent/public/assets/img/logo/white.png +0 -0
  125. stoobly_agent/public/assets/img/logo/white.svg +8 -0
  126. stoobly_agent/public/common-es2015.6f230354b12587f9be81.js +1 -0
  127. stoobly_agent/public/common-es5.6f230354b12587f9be81.js +1 -0
  128. stoobly_agent/public/favicon.ico +0 -0
  129. stoobly_agent/public/index.html +118 -0
  130. stoobly_agent/public/main-es2015.081bbd2709f22e95933e.js +1 -0
  131. stoobly_agent/public/main-es5.081bbd2709f22e95933e.js +1 -0
  132. stoobly_agent/public/polyfills-es2015.8ce2adc69f283f6c4c5e.js +1 -0
  133. stoobly_agent/public/polyfills-es5.7530172ddcec11a10eb3.js +1 -0
  134. stoobly_agent/public/runtime-es2015.362e49d383fb724f5414.js +1 -0
  135. stoobly_agent/public/runtime-es5.362e49d383fb724f5414.js +1 -0
  136. stoobly_agent/public/styles.ca104d947fbb2eebbeca.css +6 -0
  137. stoobly_agent/test/app/cli/helpers/openapi_endpoint_adapter_additional_props_test.py +34 -0
  138. stoobly_agent/test/app/cli/helpers/openapi_endpoint_adapter_missing_info_test.py +80 -3
  139. stoobly_agent/test/app/cli/helpers/openapi_endpoint_adapter_missing_oauth2_scopes_test.py +82 -1
  140. stoobly_agent/test/app/cli/helpers/openapi_endpoint_adapter_missing_servers_test.py +80 -3
  141. stoobly_agent/test/app/cli/helpers/openapi_endpoint_adapter_test.py +617 -4
  142. {stoobly_agent-0.33.6.dist-info → stoobly_agent-0.34.0.dist-info}/METADATA +1 -1
  143. {stoobly_agent-0.33.6.dist-info → stoobly_agent-0.34.0.dist-info}/RECORD +146 -20
  144. {stoobly_agent-0.33.6.dist-info → stoobly_agent-0.34.0.dist-info}/LICENSE +0 -0
  145. {stoobly_agent-0.33.6.dist-info → stoobly_agent-0.34.0.dist-info}/WHEEL +0 -0
  146. {stoobly_agent-0.33.6.dist-info → stoobly_agent-0.34.0.dist-info}/entry_points.txt +0 -0
stoobly_agent/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
1
  COMMAND = 'stoobly-agent'
2
- VERSION = '0.33.6'
2
+ VERSION = '0.34.0'
@@ -1,4 +1,4 @@
1
- import click
1
+ import click, sys
2
2
 
3
3
  from stoobly_agent.app.models.types import OPENAPI_FORMAT
4
4
  from stoobly_agent.app.settings import Settings
@@ -7,6 +7,7 @@ from stoobly_agent.lib.utils.conditional_decorator import ConditionalDecorator
7
7
 
8
8
  from .helpers.endpoint_facade import EndpointFacade
9
9
  from .helpers.endpoints_apply_context import EndpointsApplyContext
10
+ from .helpers.endpoints_import_context import EndpointsImportContext
10
11
  from .helpers.feature_flags import local, remote
11
12
  from .helpers.validations import validate_project_key, validate_scenario_key
12
13
 
@@ -54,3 +55,25 @@ def apply(**kwargs):
54
55
 
55
56
  facade = EndpointFacade(settings)
56
57
  facade.apply(context)
58
+
59
+ @endpoint.command(
60
+ "import",
61
+ help="Import endpoints"
62
+ )
63
+ @click.option('--source-format', required=True, type=click.Choice([OPENAPI_FORMAT]), help="Spec file format.")
64
+ @click.option('--source-path', help='Path to spec file.')
65
+ def import_endpoint(**kwargs):
66
+ context = EndpointsImportContext()
67
+
68
+ project_key = settings.proxy.intercept.project_key
69
+ if project_key:
70
+ project_key = validate_project_key(project_key)
71
+ context.with_project(project_key.id)
72
+
73
+ context.with_source(kwargs.get('source_path'), kwargs.get('source_format'))
74
+
75
+ endpoint_handler = lambda endpoint: print(f"{bcolors.OKBLUE}Importing Endpoint: {endpoint['method']} {endpoint['path']}{bcolors.ENDC}")
76
+ context.with_endpoint_handler(endpoint_handler)
77
+
78
+ facade = EndpointFacade(settings)
79
+ facade.import_endpoints(context)
@@ -2,9 +2,17 @@ import pdb
2
2
 
3
3
  from stoobly_agent.app.models.factories.resource.request import RequestResourceFactory
4
4
  from stoobly_agent.app.settings import Settings
5
+ from stoobly_agent.lib.api.endpoints_resource import EndpointsResource
6
+ from stoobly_agent.lib.api.header_names_resource import HeaderNamesResource
7
+ from stoobly_agent.lib.api.body_param_names_resource import BodyParamNamesResource
8
+ from stoobly_agent.lib.api.query_param_names_resource import QueryParamNamesResource
9
+ from stoobly_agent.lib.api.response_param_names_resource import ResponseParamNamesResource
10
+ from stoobly_agent.lib.api.response_header_names_resource import ResponseHeaderNamesResource
5
11
 
6
12
  from .endpoints_apply_context import EndpointsApplyContext
13
+ from .endpoints_import_context import EndpointsImportContext
7
14
  from .endpoints_apply_service import apply_endpoints
15
+ from .endpoints_import_service import import_endpoints
8
16
  from .synchronize_request_service import SynchronizeRequestService
9
17
 
10
18
  class EndpointFacade():
@@ -22,4 +30,17 @@ class EndpointFacade():
22
30
  )
23
31
  context.with_request_handler(request_handler)
24
32
 
25
- apply_endpoints(context)
33
+ apply_endpoints(context)
34
+
35
+ def import_endpoints(self, context: EndpointsImportContext):
36
+ api_url = self.__settings.remote.api_url
37
+ api_key = self.__settings.remote.api_key
38
+
39
+ context.with_endpoint_resource(EndpointsResource(api_url, api_key))
40
+ context.with_header_name_resource(HeaderNamesResource(api_url, api_key))
41
+ context.with_body_param_name_resource(BodyParamNamesResource(api_url, api_key))
42
+ context.with_query_param_name_resource(QueryParamNamesResource(api_url, api_key))
43
+ context.with_response_param_name_resource(ResponseParamNamesResource(api_url, api_key))
44
+ context.with_response_header_name_resource(ResponseHeaderNamesResource(api_url, api_key))
45
+
46
+ import_endpoints(context)
@@ -0,0 +1,57 @@
1
+ from typing import Callable, TypedDict
2
+
3
+ from .openapi_endpoint_adapter import OpenApiEndpointAdapter
4
+ from stoobly_agent.app.models.types import OPENAPI_FORMAT
5
+ from stoobly_agent.lib.api.body_param_names_resource import BodyParamNamesResource
6
+ from stoobly_agent.lib.api.endpoints_resource import EndpointsResource
7
+ from stoobly_agent.lib.api.header_names_resource import HeaderNamesResource
8
+ from stoobly_agent.lib.api.interfaces.endpoints import EndpointShowResponse
9
+ from stoobly_agent.lib.api.query_param_names_resource import QueryParamNamesResource
10
+ from stoobly_agent.lib.api.response_param_names_resource import ResponseParamNamesResource
11
+ from stoobly_agent.lib.api.response_header_names_resource import ResponseHeaderNamesResource
12
+
13
+ class EndpointsImportContext:
14
+ def __init__(self):
15
+ self.endpoints = []
16
+ self.endpoint_handlers = []
17
+ self.resources = {}
18
+ self.project_id = None
19
+
20
+ def with_endpoint_handler(self, handler: Callable[[EndpointShowResponse], None]):
21
+ self.endpoint_handlers.append(handler)
22
+ return self
23
+
24
+ def with_project(self, project_id: int):
25
+ self.project_id = project_id
26
+ return self
27
+
28
+ def with_endpoint_resource(self, resource: EndpointsResource):
29
+ self.resources['endpoint'] = resource
30
+ return self
31
+
32
+ def with_header_name_resource(self, resource: HeaderNamesResource):
33
+ self.resources['header_name'] = resource
34
+ return self
35
+
36
+ def with_body_param_name_resource(self, resource: BodyParamNamesResource):
37
+ self.resources['body_param_name'] = resource
38
+ return self
39
+
40
+ def with_query_param_name_resource(self, resource: QueryParamNamesResource):
41
+ self.resources['query_param_name'] = resource
42
+ return self
43
+
44
+ def with_response_param_name_resource(self, resource: ResponseParamNamesResource):
45
+ self.resources['response_param_name'] = resource
46
+ return self
47
+
48
+ def with_response_header_name_resource(self, resource: ResponseHeaderNamesResource):
49
+ self.resources['response_header_name'] = resource
50
+ return self
51
+
52
+ def with_source(self, path: str, format: str):
53
+ if format == OPENAPI_FORMAT:
54
+ endpoint_adapter = OpenApiEndpointAdapter()
55
+ self.endpoints += endpoint_adapter.adapt_from_file(path)
56
+
57
+ return self
@@ -0,0 +1,147 @@
1
+ import json
2
+ import requests
3
+ import sys
4
+ from typing import Dict
5
+
6
+ from .endpoints_import_context import EndpointsImportContext
7
+ from stoobly_agent.app.models.types import ENDPOINT_COMPONENT_NAMES
8
+ from stoobly_agent.lib.api.body_param_names_resource import BodyParamNamesResource
9
+ from stoobly_agent.lib.api.endpoints_resource import EndpointsResource
10
+ from stoobly_agent.lib.api.header_names_resource import HeaderNamesResource
11
+ from stoobly_agent.lib.api.interfaces.endpoints import BodyParamName
12
+ from stoobly_agent.lib.api.interfaces.endpoints import EndpointShowResponse
13
+ from stoobly_agent.lib.api.interfaces.endpoints import RequestComponentName
14
+ from stoobly_agent.lib.api.query_param_names_resource import QueryParamNamesResource
15
+ from stoobly_agent.lib.api.response_header_names_resource import ResponseHeaderNamesResource
16
+ from stoobly_agent.lib.api.response_param_names_resource import ResponseParamNamesResource
17
+ from stoobly_agent.lib.logger import bcolors
18
+
19
+ def import_endpoints(context: EndpointsImportContext):
20
+ for endpoint in context.endpoints:
21
+ for endpoint_handler in context.endpoint_handlers:
22
+ endpoint_handler(endpoint)
23
+
24
+ try:
25
+ res = import_endpoint(context.project_id, context.resources['endpoint'], endpoint)
26
+ except requests.HTTPError as e:
27
+ error_handler(endpoint, e)
28
+ return
29
+
30
+ if res.status_code == 409: # Endpoint already created
31
+ continue
32
+
33
+ endpoint_id = res.json()['id']
34
+ try:
35
+ res = import_component_names(context.project_id, endpoint_id, endpoint, context.resources)
36
+ except requests.HTTPError as e:
37
+ error_handler(endpoint, e)
38
+ try:
39
+ cleanup_endpoint(context.project_id, endpoint_id, context.resources['endpoint'])
40
+ except requests.HTTPError as e:
41
+ print(f"{bcolors.FAIL}Encountered {e.response.status_code} {e.response.reason}: {e.response.text} - Failed to delete endpoint: {endpoint['method']} {endpoint['path']}{bcolors.ENDC}", file=sys.stderr)
42
+ return
43
+
44
+ def import_endpoint(project_id: int, endpoint_resource: EndpointsResource, endpoint: EndpointShowResponse):
45
+ res: requests.Response = endpoint_resource.create(**{
46
+ 'host': endpoint.get('host'),
47
+ 'method': endpoint.get('method'),
48
+ 'path_segments': json.dumps(endpoint.get('path_segment_names', [])),
49
+ 'path': endpoint.get('path'),
50
+ 'port': endpoint.get('port'),
51
+ 'project_id': project_id,
52
+ })
53
+
54
+ if res.ok or res.status_code == 409: # Endpoint already created
55
+ return res
56
+
57
+ res.raise_for_status()
58
+
59
+ def import_header_name(project_id: int, endpoint_id: int, header_name_resource: HeaderNamesResource, header_name: RequestComponentName):
60
+ res: requests.Response = header_name_resource.create(endpoint_id, {
61
+ 'name': header_name.get('name'),
62
+ 'is_deterministic': header_name.get('is_deterministic', True),
63
+ 'is_required': header_name.get('is_required', False),
64
+ 'endpoint_id': endpoint_id,
65
+ 'project_id': project_id,
66
+ })
67
+
68
+ res.raise_for_status()
69
+
70
+ def import_body_param_name(project_id: int, endpoint_id: int, body_param_name_resource: BodyParamNamesResource, body_param_name: BodyParamName):
71
+ res: requests.Response = body_param_name_resource.create(endpoint_id, {
72
+ 'name': body_param_name.get('name'),
73
+ 'is_deterministic': body_param_name.get('is_deterministic', True),
74
+ 'is_required': body_param_name.get('is_required', False),
75
+ 'inferred_type': body_param_name.get('inferred_type'),
76
+ 'query': body_param_name.get('query'),
77
+ 'endpoint_id': endpoint_id,
78
+ 'project_id': project_id,
79
+ })
80
+
81
+ res.raise_for_status()
82
+
83
+ def import_query_param_name(project_id: int, endpoint_id: int, query_param_name_resource: QueryParamNamesResource, query_param_name: RequestComponentName):
84
+ res: requests.Response = query_param_name_resource.create(endpoint_id, {
85
+ 'name': query_param_name.get('name'),
86
+ 'is_deterministic': query_param_name.get('is_deterministic', True),
87
+ 'is_required': query_param_name.get('is_required', False),
88
+ 'endpoint_id': endpoint_id,
89
+ 'project_id': project_id,
90
+ })
91
+
92
+ res.raise_for_status()
93
+
94
+ def import_response_param_name(project_id: int, endpoint_id: int, response_param_name_resource: ResponseParamNamesResource, response_param_name: RequestComponentName):
95
+ res: requests.Response = response_param_name_resource.create(endpoint_id, {
96
+ 'name': response_param_name.get('name'),
97
+ 'is_deterministic': response_param_name.get('is_deterministic', True),
98
+ 'is_required': response_param_name.get('is_required', False),
99
+ 'inferred_type': response_param_name.get('inferred_type'),
100
+ 'query': response_param_name.get('query'),
101
+ 'endpoint_id': endpoint_id,
102
+ 'project_id': project_id,
103
+ })
104
+
105
+ res.raise_for_status()
106
+
107
+ def import_response_header_name(project_id: int, endpoint_id: int, response_header_name_resource: ResponseHeaderNamesResource, response_header_name: RequestComponentName):
108
+ res: requests.Response = response_header_name_resource.create(endpoint_id, {
109
+ 'name': response_header_name.get('name'),
110
+ 'is_deterministic': response_header_name.get('is_deterministic', True),
111
+ 'is_required': response_header_name.get('is_required', False),
112
+ 'endpoint_id': endpoint_id,
113
+ 'project_id': project_id,
114
+ })
115
+
116
+ res.raise_for_status()
117
+
118
+ component_name_import_dispatch = {
119
+ ENDPOINT_COMPONENT_NAMES[0]: import_header_name,
120
+ ENDPOINT_COMPONENT_NAMES[1]: import_body_param_name,
121
+ ENDPOINT_COMPONENT_NAMES[2]: import_query_param_name,
122
+ ENDPOINT_COMPONENT_NAMES[3]: import_response_header_name,
123
+ ENDPOINT_COMPONENT_NAMES[4]: import_response_param_name
124
+ }
125
+
126
+ def process_import(component_name: str, *args):
127
+ return component_name_import_dispatch[component_name](*args)
128
+
129
+ def import_component_names(project_id: int, endpoint_id: int, endpoint: EndpointShowResponse, resources: Dict[str, EndpointsResource]):
130
+ for component_name in ENDPOINT_COMPONENT_NAMES:
131
+ for component in endpoint.get(f'{component_name}s', {}):
132
+ resource = resources[component_name]
133
+ process_import(component_name, project_id, endpoint_id, resource, component)
134
+
135
+ def cleanup_endpoint(project_id: int, endpoint_id: int, resource: EndpointsResource):
136
+ print(f"{bcolors.FAIL}Cleaning up partial import...{bcolors.ENDC}")
137
+ res: requests.Response = resource.destroy(endpoint_id, **{
138
+ 'project_id': project_id
139
+ })
140
+
141
+ res.raise_for_status()
142
+
143
+ def error_handler(endpoint: EndpointShowResponse, error: requests.HTTPError):
144
+ print(
145
+ f"{bcolors.FAIL}Encountered {error.response.status_code} {error.response.reason}: {error.response.text} - Failed to import endpoint: {endpoint['method']} {endpoint['path']} - Aborting operation{bcolors.ENDC}",
146
+ file=sys.stderr
147
+ )
@@ -73,19 +73,32 @@ class OpenApiEndpointAdapter():
73
73
  endpoint: EndpointShowResponse = {}
74
74
  endpoint['id'] = endpoint_counter
75
75
  endpoint['method'] = http_method.upper()
76
- endpoint['host'] = parsed_url.netloc
76
+ endpoint['host'] = '-' if parsed_url.netloc == '' else parsed_url.netloc
77
77
 
78
78
  joined_path = self.__urljoin(parsed_url.path, path_name)
79
79
  split_parts = joined_path.split('/')
80
80
  pattern_path = []
81
+ segment_names = []
81
82
  for part in split_parts:
82
83
  sanitized_part = part
84
+ segment_name = part
83
85
  if part.startswith('{') and part.endswith('}'):
84
86
  sanitized_part = '%'
87
+ segment_name = f":{part[1:-1]}"
85
88
  pattern_path.append(sanitized_part)
89
+ segment_names.append(segment_name)
86
90
  pattern_path_str = '/'.join(pattern_path)
87
91
  endpoint['match_pattern'] = pattern_path_str
88
92
  endpoint['path'] = joined_path
93
+
94
+ endpoint['path_segment_names'] = []
95
+ for segment_name in segment_names:
96
+ if segment_name == "":
97
+ continue
98
+ path_component_name: RequestComponentName = {}
99
+ path_component_name['name'] = segment_name
100
+ path_component_name['type'] = "alias" if segment_name.startswith(':') else "static"
101
+ endpoint['path_segment_names'].append(path_component_name)
89
102
 
90
103
  endpoint['port'] = str(parsed_url.port)
91
104
  if endpoint['port'] is None or endpoint['port'] == 'None':
@@ -94,7 +107,7 @@ class OpenApiEndpointAdapter():
94
107
  elif parsed_url.scheme == 'http':
95
108
  endpoint['port'] = '80'
96
109
  else:
97
- endpoint['port'] = ''
110
+ endpoint['port'] = '0'
98
111
 
99
112
  alias_counter = 0
100
113
  header_param_counter = 0
@@ -142,7 +155,10 @@ class OpenApiEndpointAdapter():
142
155
  param_value = self.__open_api_to_default_python_type(open_api_type)
143
156
 
144
157
  literal_query_param = {
145
- parameter['name']: {'value': param_value}
158
+ parameter['name']: {
159
+ 'value': param_value,
160
+ 'required': parameter.get('required', False),
161
+ }
146
162
  }
147
163
 
148
164
  if not endpoint.get('literal_query_params'):
@@ -238,6 +254,11 @@ class OpenApiEndpointAdapter():
238
254
  else:
239
255
  self.__convert_literal_component_param(endpoint, required_body_params, [literal_body_params], 'body_param_name', 'literal_body_params')
240
256
 
257
+ # Responses -> construct lists of response header and response param name resources
258
+ responses = operation.get('responses', {})
259
+ if responses:
260
+ self.__parse_responses(endpoint, responses, components)
261
+
241
262
  endpoints.append(endpoint)
242
263
 
243
264
  return endpoints
@@ -391,11 +412,30 @@ class OpenApiEndpointAdapter():
391
412
  component_name = component_data[1]
392
413
  component = components.get(component_type, {})
393
414
 
415
+ # If component_type is 'headers'
416
+ # Example: '#components/headers/X-RateLimit-Limit'
417
+ if component_type == "headers":
418
+ # In this case, literal_body_params represents a header rather than request or response body params
419
+ literal_body_params['name'] = component_name
420
+ header_example = component.get('example')
421
+ if header_example:
422
+ literal_body_params['values'].append(header_example)
423
+ literal_body_params['is_required'] = component.get('is_required', False)
424
+ literal_body_params['is_deterministic'] = True
425
+ return literal_body_params
426
+
394
427
  # Example: {'type': 'object', 'required': ['name'], 'properties': {'name': {'type': 'string'}, 'tag': {'type': 'string'}}}
395
428
  body_spec = component.content()[component_name]
396
429
  required_body_params += body_spec.get('required', [])
397
430
 
398
- param_properties = body_spec.get('properties')
431
+ param_properties = {}
432
+ schema_type = body_spec.get('type')
433
+ if schema_type:
434
+ if schema_type == 'object':
435
+ param_properties = body_spec.get('properties', {})
436
+ elif schema_type == 'array':
437
+ param_properties = {'tmp': body_spec['items']}
438
+
399
439
  all_of = body_spec.get('allOf')
400
440
  any_of = body_spec.get('anyOf')
401
441
  one_of = body_spec.get('oneOf')
@@ -421,7 +461,7 @@ class OpenApiEndpointAdapter():
421
461
  # elif any_of or one_of:
422
462
 
423
463
  return param_properties
424
-
464
+
425
465
  def __convert_literal_component_param(self, endpoint: EndpointShowResponse,
426
466
  required_component_params: List[str], literal_component_params: Union[dict, list],
427
467
  component_name: str, literal_component_name: str) -> None:
@@ -444,7 +484,11 @@ class OpenApiEndpointAdapter():
444
484
  else:
445
485
  param['is_required'] = False
446
486
 
447
- endpoint[component_name + 's'] = built_params_list
487
+ if not endpoint.get(component_name + 's'):
488
+ endpoint[component_name + 's'] = built_params_list
489
+ else:
490
+ endpoint[component_name + 's'].extend(built_params_list)
491
+
448
492
  del endpoint[literal_component_name]
449
493
 
450
494
  # urllib.parse.urljoin() doesn't work for some of our edge cases
@@ -557,3 +601,64 @@ class OpenApiEndpointAdapter():
557
601
 
558
602
  return result
559
603
 
604
+ def __parse_responses(self, endpoint: EndpointShowResponse, responses: Spec, components: Spec):
605
+ for response_code, response_definition in responses.items():
606
+ # Construct response param name components
607
+ literal_response_params = {}
608
+ response_body_array = False
609
+ required_response_params = []
610
+ response_content = response_definition.get('content', {})
611
+ for mimetype, media_type in response_content.items():
612
+ param_properties = {}
613
+ schema = media_type['schema']
614
+
615
+ if '$ref' in schema:
616
+ reference = schema['$ref']
617
+ self.__dereference(components, reference, required_response_params, literal_response_params)
618
+ else:
619
+ schema_type = schema.get('type')
620
+ if schema_type:
621
+ if schema_type == 'object':
622
+ param_properties = schema.get('properties', {})
623
+ elif schema_type == 'array':
624
+ response_body_array = True
625
+ param_properties = {'tmp': schema['items']}
626
+
627
+ for property_key, property_value in param_properties.items():
628
+ if property_key in required_response_params:
629
+ param_properties[property_key]['required'] = True
630
+
631
+ self.__extract_param_properties(components, None, required_response_params, param_properties, literal_response_params)
632
+
633
+ if literal_response_params:
634
+ endpoint['literal_response_params'] = literal_response_params
635
+
636
+ # Only support first media type
637
+ break
638
+
639
+ literal_response_params = endpoint.get('literal_response_params')
640
+ if literal_response_params:
641
+ if not response_body_array:
642
+ self.__convert_literal_component_param(endpoint, required_response_params, literal_response_params, 'response_param_name', 'literal_response_params')
643
+ else:
644
+ self.__convert_literal_component_param(endpoint, required_response_params, [literal_response_params], 'response_param_name', 'literal_response_params')
645
+
646
+ # Construct response header name components
647
+ response_headers = response_definition.get('headers', {})
648
+ for header_name, header_definition in response_headers.items():
649
+ response_header_name: RequestComponentName = {}
650
+ response_header_name['name'] = header_name
651
+
652
+ if '$ref' in header_definition:
653
+ reference = header_definition['$ref']
654
+ self.__dereference(components, reference, [], response_header_name)
655
+ else:
656
+ header_example = header_definition.get('example')
657
+ if header_example:
658
+ response_header_name['values'].append(header_example)
659
+ response_header_name['is_required'] = header_definition.get('is_required', False)
660
+ response_header_name['is_deterministic'] = True
661
+
662
+ if not endpoint.get('response_header_names'):
663
+ endpoint['response_header_names'] = []
664
+ endpoint['response_header_names'].append(response_header_name)
@@ -1,7 +1,33 @@
1
- from typing import Literal, TypedDict
1
+ from typing import TypedDict
2
2
 
3
3
  OPENAPI_FORMAT = 'openapi'
4
4
 
5
+ ENDPOINT_COMPONENT_NAMES = [
6
+ "header_name",
7
+ "body_param_name",
8
+ "query_param_name",
9
+ "response_header_name",
10
+ "response_param_name"
11
+ ]
12
+
5
13
  class EndpointCreateParams(TypedDict):
6
- format: Literal[f"{OPENAPI_FORMAT}"]
14
+ host: str
15
+ method: str
16
+ path_segments: str
7
17
  path: str
18
+ port: str
19
+ project_id: str
20
+
21
+ class HeaderNameCreateParams(TypedDict):
22
+ name: str
23
+ is_required: bool
24
+ is_deterministic: bool
25
+ project_id: str
26
+ endpoint_id: int
27
+
28
+ class ParamNameCreateParams(TypedDict):
29
+ name: str
30
+ project_id: str
31
+ endpoint_id: int
32
+ inferred_type: str
33
+ query: str
@@ -199,7 +199,7 @@ class MitmproxyRequestFacade(Request):
199
199
  self.request.port = int(rewrite.port)
200
200
 
201
201
  if rewrite.scheme:
202
- self.request.scheme = rewrite.scheme
202
+ self.request.scheme = rewrite.scheme.lower()
203
203
 
204
204
  def __rewrite_headers(self, rewrites: List[ParameterRule]):
205
205
  self.__apply_headers(rewrites, self.__rewrite_handler)
@@ -42,7 +42,7 @@ class InterceptSettings:
42
42
 
43
43
  @mode.setter
44
44
  def mode(self, v):
45
- if v in [mode.MOCK, mode.NONE, mode.RECORD, mode.TEST]:
45
+ if v in [mode.MOCK, mode.NONE, mode.RECORD, mode.REPLAY, mode.TEST]:
46
46
  self.__mode = v
47
47
  self.__intercept_settings['mode'] = v
48
48
 
@@ -69,3 +69,7 @@ class Api():
69
69
  def put(self, url, **kwargs):
70
70
  handler = lambda: requests.put(url, **kwargs)
71
71
  return self.without_proxy(handler)
72
+
73
+ def delete(self, url, **kwargs):
74
+ handler = lambda: requests.delete(url, **kwargs)
75
+ return self.without_proxy(handler)
@@ -0,0 +1,36 @@
1
+ import requests
2
+ import urllib
3
+ import pdb
4
+
5
+ from stoobly_agent.app.models.types import ParamNameCreateParams
6
+
7
+ from ..logger import Logger
8
+ from .endpoints_resource import EndpointsResource
9
+
10
+ class BodyParamNamesResource(EndpointsResource):
11
+ BODY_PARAM_NAMES_ENDPOINT = 'body_param_names'
12
+
13
+ def create(self, endpoint_id: int, params: ParamNameCreateParams = {}):
14
+ url = f"{self.service_url}/{self.ENDPOINTS_ENDPOINT}/{endpoint_id}/{self.BODY_PARAM_NAMES_ENDPOINT}"
15
+ return self.post(url, headers=self.default_headers, json=params)
16
+
17
+ def index(self, endpoint_id: int, query_params = {}) -> requests.Response:
18
+ url = f"{self.service_url}/{self.ENDPOINTS_ENDPOINT}/{endpoint_id}/{self.BODY_PARAM_NAMES_ENDPOINT}"
19
+
20
+ Logger.instance().debug(f"{self.LOG_ID}.request_response:{url}?{urllib.parse.urlencode(query_params)}")
21
+
22
+ return self.get(url, headers=self.default_headers, params=query_params)
23
+
24
+ def show(self, endpoint_id: int, body_param_name_id: int, query_params = {}) -> requests.Response:
25
+ url = f"{self.service_url}/{self.ENDPOINTS_ENDPOINT}/{endpoint_id}/{self.BODY_PARAM_NAMES_ENDPOINT}/{body_param_name_id}"
26
+
27
+ Logger.instance().debug(f"{self.LOG_ID}.request_response:{url}?{urllib.parse.urlencode(query_params)}")
28
+
29
+ return self.get(url, headers=self.default_headers, params=query_params)
30
+
31
+ def destroy(self, endpoint_id, body_param_name_id: int, query_params = {}) -> requests.Response:
32
+ url = f"{self.service_url}/{self.ENDPOINTS_ENDPOINT}/{endpoint_id}/{self.BODY_PARAM_NAMES_ENDPOINT}/{body_param_name_id}"
33
+
34
+ Logger.instance().debug(f"{self.LOG_ID}.request_response:{url}?{urllib.parse.urlencode(query_params)}")
35
+
36
+ return self.delete(url, headers=self.default_headers, params=query_params)
@@ -11,7 +11,7 @@ class EndpointsResource(StooblyApi):
11
11
 
12
12
  def create(self, **params):
13
13
  url = f"{self.service_url}/{self.ENDPOINTS_ENDPOINT}"
14
- return self.post(url, headers=self.default_headers, data=params)
14
+ return self.post(url, headers=self.default_headers, json=params)
15
15
 
16
16
  def index(self, **query_params: EndpointsIndexQueryParams) -> requests.Response:
17
17
  url = f"{self.service_url}/{self.ENDPOINTS_ENDPOINT}"
@@ -25,4 +25,11 @@ class EndpointsResource(StooblyApi):
25
25
 
26
26
  Logger.instance().debug(f"{self.LOG_ID}.request_response:{url}?{urllib.parse.urlencode(query_params)}")
27
27
 
28
- return self.get(url, headers=self.default_headers, params=query_params)
28
+ return self.get(url, headers=self.default_headers, params=query_params)
29
+
30
+ def destroy(self, endpoint_id: int, **query_params) -> requests.Response:
31
+ url = f"{self.service_url}/{self.ENDPOINTS_ENDPOINT}/{endpoint_id}"
32
+
33
+ Logger.instance().debug(f"{self.LOG_ID}.request_response:{url}?{urllib.parse.urlencode(query_params)}")
34
+
35
+ return self.delete(url, headers=self.default_headers, params=query_params)
@@ -0,0 +1,36 @@
1
+ import requests
2
+ import urllib
3
+ import pdb
4
+
5
+ from stoobly_agent.app.models.types import HeaderNameCreateParams
6
+
7
+ from ..logger import Logger
8
+ from .endpoints_resource import EndpointsResource
9
+
10
+ class HeaderNamesResource(EndpointsResource):
11
+ HEADER_NAMES_ENDPOINT = 'header_names'
12
+
13
+ def create(self, endpoint_id: int, params: HeaderNameCreateParams = {}):
14
+ url = f"{self.service_url}/{self.ENDPOINTS_ENDPOINT}/{endpoint_id}/{self.HEADER_NAMES_ENDPOINT}"
15
+ return self.post(url, headers=self.default_headers, json=params)
16
+
17
+ def index(self, endpoint_id: int, query_params = {}) -> requests.Response:
18
+ url = f"{self.service_url}/{self.ENDPOINTS_ENDPOINT}/{endpoint_id}/{self.HEADER_NAMES_ENDPOINT}"
19
+
20
+ Logger.instance().debug(f"{self.LOG_ID}.request_response:{url}?{urllib.parse.urlencode(query_params)}")
21
+
22
+ return self.get(url, headers=self.default_headers, params=query_params)
23
+
24
+ def show(self, endpoint_id: int, header_name_id: int, query_params = {}) -> requests.Response:
25
+ url = f"{self.service_url}/{self.ENDPOINTS_ENDPOINT}/{endpoint_id}/{self.HEADER_NAMES_ENDPOINT}/{header_name_id}"
26
+
27
+ Logger.instance().debug(f"{self.LOG_ID}.request_response:{url}?{urllib.parse.urlencode(query_params)}")
28
+
29
+ return self.get(url, headers=self.default_headers, params=query_params)
30
+
31
+ def destroy(self, endpoint_id: int, header_name_id: int, query_params = {}) -> requests.Response:
32
+ url = f"{self.service_url}/{self.ENDPOINTS_ENDPOINT}/{endpoint_id}/{self.HEADER_NAMES_ENDPOINT}/{header_name_id}"
33
+
34
+ Logger.instance().debug(f"{self.LOG_ID}.request_response:{url}?{urllib.parse.urlencode(query_params)}")
35
+
36
+ return self.delete(url, headers=self.default_headers, params=query_params)
@@ -43,8 +43,10 @@ class EndpointShowResponse(TypedDict):
43
43
  path_segment_names: List[RequestComponentName]
44
44
  query_param_names: List[RequestComponentName]
45
45
  response_param_names: List[ResponseParamName]
46
+ response_header_names: List[RequestComponentName]
46
47
  literal_query_params: Optional[dict]
47
48
  literal_body_params: Optional[dict]
49
+ literal_response_params: Optional[dict]
48
50
 
49
51
  ARRAY_TYPE = 'Array'
50
52