stoobly-agent 0.33.7__py3-none-any.whl → 0.34.1__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 (164) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/cli/dev_tools_cli.py +1 -0
  3. stoobly_agent/app/cli/endpoint_cli.py +25 -2
  4. stoobly_agent/app/cli/helpers/endpoint_facade.py +22 -1
  5. stoobly_agent/app/cli/helpers/endpoints_import_context.py +57 -0
  6. stoobly_agent/app/cli/helpers/endpoints_import_service.py +152 -0
  7. stoobly_agent/app/cli/helpers/openapi_endpoint_adapter.py +111 -6
  8. stoobly_agent/app/cli/helpers/replay_facade.py +4 -0
  9. stoobly_agent/app/cli/request_cli.py +2 -0
  10. stoobly_agent/app/cli/scenario_cli.py +2 -0
  11. stoobly_agent/app/cli/types/test.py +2 -0
  12. stoobly_agent/app/models/factories/resource/local_db/helpers/request_builder.py +1 -1
  13. stoobly_agent/app/models/types/endpoint.py +28 -2
  14. stoobly_agent/app/proxy/handle_mock_service.py +15 -3
  15. stoobly_agent/app/proxy/intercept_settings.py +39 -0
  16. stoobly_agent/app/proxy/mock/eval_fixtures_service.py +61 -0
  17. stoobly_agent/app/proxy/mock/types/__init__.py +10 -0
  18. stoobly_agent/app/proxy/replay/replay_request_service.py +11 -3
  19. stoobly_agent/app/proxy/run.py +6 -0
  20. stoobly_agent/app/proxy/test/context.py +12 -0
  21. stoobly_agent/app/proxy/test/context_abc.py +15 -0
  22. stoobly_agent/app/settings/intercept_settings.py +1 -1
  23. stoobly_agent/cli.py +13 -2
  24. stoobly_agent/config/constants/custom_headers.py +2 -0
  25. stoobly_agent/config/constants/env_vars.py +2 -0
  26. stoobly_agent/lib/api/api.py +4 -0
  27. stoobly_agent/lib/api/body_param_names_resource.py +36 -0
  28. stoobly_agent/lib/api/endpoints_resource.py +9 -2
  29. stoobly_agent/lib/api/header_names_resource.py +36 -0
  30. stoobly_agent/lib/api/interfaces/endpoints.py +2 -0
  31. stoobly_agent/lib/api/query_param_names_resource.py +36 -0
  32. stoobly_agent/lib/api/response_header_names_resource.py +36 -0
  33. stoobly_agent/lib/api/response_param_names_resource.py +36 -0
  34. stoobly_agent/public/0-es2015.a7e60cafc0868f87a771.js +1 -0
  35. stoobly_agent/public/0-es5.a7e60cafc0868f87a771.js +1 -0
  36. stoobly_agent/public/1-es2015.cb071776436f10954db5.js +1 -0
  37. stoobly_agent/public/1-es5.cb071776436f10954db5.js +1 -0
  38. stoobly_agent/public/10-es2015.bb1702aaf3968a2cb521.js +1 -0
  39. stoobly_agent/public/10-es5.bb1702aaf3968a2cb521.js +1 -0
  40. stoobly_agent/public/12-es2015.591ec692afb8f8d13842.js +1 -0
  41. stoobly_agent/public/12-es5.591ec692afb8f8d13842.js +1 -0
  42. stoobly_agent/public/13-es2015.f381e7d6ff361b669e12.js +1 -0
  43. stoobly_agent/public/13-es5.f381e7d6ff361b669e12.js +1 -0
  44. stoobly_agent/public/14-es2015.1ffb225a16d6292dbddd.js +1 -0
  45. stoobly_agent/public/14-es5.1ffb225a16d6292dbddd.js +1 -0
  46. stoobly_agent/public/15-es2015.2cf39bcaeb0ea2c52297.js +1 -0
  47. stoobly_agent/public/15-es5.2cf39bcaeb0ea2c52297.js +1 -0
  48. stoobly_agent/public/16-es2015.0e9422b274e642f95e41.js +1 -0
  49. stoobly_agent/public/16-es5.0e9422b274e642f95e41.js +1 -0
  50. stoobly_agent/public/17-es2015.7fb08367a22d1e34aed7.js +1 -0
  51. stoobly_agent/public/17-es5.7fb08367a22d1e34aed7.js +1 -0
  52. stoobly_agent/public/18-es2015.5caa4789d1c335e628f2.js +1 -0
  53. stoobly_agent/public/18-es5.5caa4789d1c335e628f2.js +1 -0
  54. stoobly_agent/public/19-es2015.8049aef58c329c500f9b.js +1 -0
  55. stoobly_agent/public/19-es5.8049aef58c329c500f9b.js +1 -0
  56. stoobly_agent/public/2-es2015.8f184ac63348ba447b1f.js +1 -0
  57. stoobly_agent/public/2-es5.8f184ac63348ba447b1f.js +1 -0
  58. stoobly_agent/public/20-es2015.473486aabfa4d4a6431b.js +1 -0
  59. stoobly_agent/public/20-es5.473486aabfa4d4a6431b.js +1 -0
  60. stoobly_agent/public/21-es2015.63ed4e6b242fbc047bd6.js +1 -0
  61. stoobly_agent/public/21-es5.63ed4e6b242fbc047bd6.js +1 -0
  62. stoobly_agent/public/22-es2015.df183804415330639987.js +1 -0
  63. stoobly_agent/public/22-es5.df183804415330639987.js +1 -0
  64. stoobly_agent/public/23-es2015.0da63b056fabde91bb0b.js +1 -0
  65. stoobly_agent/public/23-es5.0da63b056fabde91bb0b.js +1 -0
  66. stoobly_agent/public/28-es2015.9fe030e9d3b0e52239aa.js +1 -0
  67. stoobly_agent/public/28-es5.9fe030e9d3b0e52239aa.js +1 -0
  68. stoobly_agent/public/29-es2015.9b440f22de725732e5ab.js +1 -0
  69. stoobly_agent/public/29-es5.9b440f22de725732e5ab.js +1 -0
  70. stoobly_agent/public/30-es2015.6ad2a5126b0a27c1e7c6.js +1 -0
  71. stoobly_agent/public/30-es5.6ad2a5126b0a27c1e7c6.js +1 -0
  72. stoobly_agent/public/31-es2015.f4291150f35d54ff19ca.js +1 -0
  73. stoobly_agent/public/31-es5.f4291150f35d54ff19ca.js +1 -0
  74. stoobly_agent/public/32-es2015.c8f6ccb43bdba0adf199.js +1 -0
  75. stoobly_agent/public/32-es5.c8f6ccb43bdba0adf199.js +1 -0
  76. stoobly_agent/public/33-es2015.f985e93402ebf86322ef.js +1 -0
  77. stoobly_agent/public/33-es5.f985e93402ebf86322ef.js +1 -0
  78. stoobly_agent/public/34-es2015.0e1961fb3cf649a52d49.js +1 -0
  79. stoobly_agent/public/34-es5.0e1961fb3cf649a52d49.js +1 -0
  80. stoobly_agent/public/35-es2015.a4ae56a89c0324397624.js +1 -0
  81. stoobly_agent/public/35-es5.a4ae56a89c0324397624.js +1 -0
  82. stoobly_agent/public/36-es2015.b8fdd25590a79c820119.js +1 -0
  83. stoobly_agent/public/36-es5.b8fdd25590a79c820119.js +1 -0
  84. stoobly_agent/public/37-es2015.6567a0ce4cf87ad7287b.js +1 -0
  85. stoobly_agent/public/37-es5.6567a0ce4cf87ad7287b.js +1 -0
  86. stoobly_agent/public/38-es2015.6f6d751bff41d84d727a.js +1 -0
  87. stoobly_agent/public/38-es5.6f6d751bff41d84d727a.js +1 -0
  88. stoobly_agent/public/39-es2015.47f63177e7d4509a22fa.js +1 -0
  89. stoobly_agent/public/39-es5.47f63177e7d4509a22fa.js +1 -0
  90. stoobly_agent/public/3rdpartylicenses.txt +2418 -0
  91. stoobly_agent/public/4-es2015.182e1ce1811ef67571fb.js +1 -0
  92. stoobly_agent/public/4-es5.182e1ce1811ef67571fb.js +1 -0
  93. stoobly_agent/public/40-es2015.5333067cdc4077c7495a.js +1 -0
  94. stoobly_agent/public/40-es5.5333067cdc4077c7495a.js +1 -0
  95. stoobly_agent/public/41-es2015.7a2434380c81c11ff2c5.js +1 -0
  96. stoobly_agent/public/41-es5.7a2434380c81c11ff2c5.js +1 -0
  97. stoobly_agent/public/42-es2015.5dde662fe1e3b4e4bdd1.js +1 -0
  98. stoobly_agent/public/42-es5.5dde662fe1e3b4e4bdd1.js +1 -0
  99. stoobly_agent/public/43-es2015.608e917d689b9bb762cb.js +1 -0
  100. stoobly_agent/public/43-es5.608e917d689b9bb762cb.js +1 -0
  101. stoobly_agent/public/44-es2015.2ae5fea15b5e8c2681d3.js +1 -0
  102. stoobly_agent/public/44-es5.2ae5fea15b5e8c2681d3.js +1 -0
  103. stoobly_agent/public/45-es2015.e46f228c795174428515.js +1 -0
  104. stoobly_agent/public/45-es5.e46f228c795174428515.js +1 -0
  105. stoobly_agent/public/46-es2015.22a0d8e0b4bbfb513741.js +1 -0
  106. stoobly_agent/public/46-es5.22a0d8e0b4bbfb513741.js +1 -0
  107. stoobly_agent/public/47-es2015.3878e5d1d692107748f3.js +1 -0
  108. stoobly_agent/public/47-es5.3878e5d1d692107748f3.js +1 -0
  109. stoobly_agent/public/5-es2015.aba7173be56fc19b4b6f.js +1 -0
  110. stoobly_agent/public/5-es5.aba7173be56fc19b4b6f.js +1 -0
  111. stoobly_agent/public/6-es2015.5fb726c0555664300974.js +1 -0
  112. stoobly_agent/public/6-es5.5fb726c0555664300974.js +1 -0
  113. stoobly_agent/public/7-es2015.9b9ab4adf24d13bdc2f8.js +1 -0
  114. stoobly_agent/public/7-es5.9b9ab4adf24d13bdc2f8.js +1 -0
  115. stoobly_agent/public/8-es2015.cdd7dce2a24aaf9d974f.js +1 -0
  116. stoobly_agent/public/8-es5.cdd7dce2a24aaf9d974f.js +1 -0
  117. stoobly_agent/public/9-es2015.cdde98f2537997afbf0f.js +1 -0
  118. stoobly_agent/public/9-es5.cdde98f2537997afbf0f.js +1 -0
  119. stoobly_agent/public/CHANGELOG.md +58 -0
  120. stoobly_agent/public/README.md +264 -0
  121. stoobly_agent/public/_redirects +1 -0
  122. stoobly_agent/public/assets/img/demo/1.jpg +0 -0
  123. stoobly_agent/public/assets/img/demo/2.jpg +0 -0
  124. stoobly_agent/public/assets/img/demo/3.jpg +0 -0
  125. stoobly_agent/public/assets/img/demo/4.jpg +0 -0
  126. stoobly_agent/public/assets/img/demo/5.jpg +0 -0
  127. stoobly_agent/public/assets/img/demo/6.jpg +0 -0
  128. stoobly_agent/public/assets/img/demo/7.jpg +0 -0
  129. stoobly_agent/public/assets/img/demo/8.jpg +0 -0
  130. stoobly_agent/public/assets/img/demo/landscape.jpg +0 -0
  131. stoobly_agent/public/assets/img/demo/mountain-cinematic.jpg +0 -0
  132. stoobly_agent/public/assets/img/illustrations/checklist.svg +164 -0
  133. stoobly_agent/public/assets/img/illustrations/data_center.svg +150 -0
  134. stoobly_agent/public/assets/img/illustrations/idea.svg +213 -0
  135. stoobly_agent/public/assets/img/illustrations/it_support.svg +168 -0
  136. stoobly_agent/public/assets/img/illustrations/peak_mountain_3.svg +262 -0
  137. stoobly_agent/public/assets/img/illustrations/under_constructions_1.svg +282 -0
  138. stoobly_agent/public/assets/img/logo/colored.png +0 -0
  139. stoobly_agent/public/assets/img/logo/colored.svg +9 -0
  140. stoobly_agent/public/assets/img/logo/white.png +0 -0
  141. stoobly_agent/public/assets/img/logo/white.svg +8 -0
  142. stoobly_agent/public/common-es2015.6f230354b12587f9be81.js +1 -0
  143. stoobly_agent/public/common-es5.6f230354b12587f9be81.js +1 -0
  144. stoobly_agent/public/favicon.ico +0 -0
  145. stoobly_agent/public/index.html +118 -0
  146. stoobly_agent/public/main-es2015.081bbd2709f22e95933e.js +1 -0
  147. stoobly_agent/public/main-es5.081bbd2709f22e95933e.js +1 -0
  148. stoobly_agent/public/polyfills-es2015.8ce2adc69f283f6c4c5e.js +1 -0
  149. stoobly_agent/public/polyfills-es5.7530172ddcec11a10eb3.js +1 -0
  150. stoobly_agent/public/runtime-es2015.362e49d383fb724f5414.js +1 -0
  151. stoobly_agent/public/runtime-es5.362e49d383fb724f5414.js +1 -0
  152. stoobly_agent/public/styles.ca104d947fbb2eebbeca.css +6 -0
  153. stoobly_agent/test/app/cli/helpers/openapi_endpoint_adapter_additional_props_test.py +34 -0
  154. stoobly_agent/test/app/cli/helpers/openapi_endpoint_adapter_missing_info_test.py +80 -3
  155. stoobly_agent/test/app/cli/helpers/openapi_endpoint_adapter_missing_oauth2_scopes_test.py +82 -1
  156. stoobly_agent/test/app/cli/helpers/openapi_endpoint_adapter_missing_servers_test.py +80 -3
  157. stoobly_agent/test/app/cli/helpers/openapi_endpoint_adapter_test.py +617 -4
  158. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  159. stoobly_agent/test/app/proxy/mock/eval_fixtures_service_test.py +95 -0
  160. {stoobly_agent-0.33.7.dist-info → stoobly_agent-0.34.1.dist-info}/METADATA +1 -1
  161. {stoobly_agent-0.33.7.dist-info → stoobly_agent-0.34.1.dist-info}/RECORD +164 -35
  162. {stoobly_agent-0.33.7.dist-info → stoobly_agent-0.34.1.dist-info}/LICENSE +0 -0
  163. {stoobly_agent-0.33.7.dist-info → stoobly_agent-0.34.1.dist-info}/WHEEL +0 -0
  164. {stoobly_agent-0.33.7.dist-info → stoobly_agent-0.34.1.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.7'
2
+ VERSION = '0.34.1'
@@ -5,6 +5,7 @@ import sys
5
5
  from stoobly_agent import VERSION
6
6
  from stoobly_agent.app.proxy.replay.body_parser_service import decode_response, is_traversable
7
7
  from stoobly_agent.app.models.adapters.raw_http_response_adapter import RawHttpResponseAdapter
8
+ from stoobly_agent.lib.api.response_param_names_resource import ResponseParamNamesResource
8
9
  from stoobly_agent.lib.orm.migrate_service import migrate as database_migrate, rollback as database_rollback
9
10
  from stoobly_agent.lib.orm.replayed_response import ReplayedResponse
10
11
  from stoobly_agent.lib.orm.request import Request
@@ -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
 
@@ -46,7 +47,7 @@ def apply(**kwargs):
46
47
  context.with_lifecycle_hooks_path(kwargs.get('lifecycle_hooks_path'))
47
48
  context.with_source(kwargs.get('source_path'), kwargs.get('source_format'))
48
49
 
49
- endpoint_handler = lambda endpoint: print(f"{bcolors.OKBLUE}Applying Endpoint: {endpoint['path']}{bcolors.ENDC}")
50
+ endpoint_handler = lambda endpoint: print(f"{bcolors.OKBLUE}Applying Endpoint {endpoint['method']} {endpoint['path']}{bcolors.ENDC}")
50
51
  context.with_endpoint_handler(endpoint_handler)
51
52
 
52
53
  request_handler = lambda request, endpoint: print(request.url)
@@ -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,152 @@
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(e, f"Failed to import endpoint: {endpoint['method']} {endpoint['path']}")
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(e, f"Failed to import endpoint: {endpoint['method']} {endpoint['path']}")
38
+
39
+ try:
40
+ cleanup_endpoint(context.project_id, endpoint_id, context.resources['endpoint'])
41
+ except requests.HTTPError as e:
42
+ error_handler(e, f"Failed to delete endpoint: {endpoint['method']} {endpoint['path']}, id {endpoint_id}")
43
+ return
44
+
45
+ def import_endpoint(project_id: int, endpoint_resource: EndpointsResource, endpoint: EndpointShowResponse):
46
+ res: requests.Response = endpoint_resource.create(**{
47
+ 'host': endpoint.get('host'),
48
+ 'method': endpoint.get('method'),
49
+ 'path_segments': json.dumps(endpoint.get('path_segment_names', [])),
50
+ 'path': endpoint.get('path'),
51
+ 'port': endpoint.get('port'),
52
+ 'project_id': project_id,
53
+ })
54
+
55
+ if res.ok or res.status_code == 409: # Endpoint already created
56
+ return res
57
+
58
+ res.raise_for_status()
59
+
60
+ def import_header_name(project_id: int, endpoint_id: int, header_name_resource: HeaderNamesResource, header_name: RequestComponentName):
61
+ res: requests.Response = header_name_resource.create(endpoint_id, {
62
+ 'name': header_name.get('name'),
63
+ 'is_deterministic': header_name.get('is_deterministic', True),
64
+ 'is_required': header_name.get('is_required', False),
65
+ 'endpoint_id': endpoint_id,
66
+ 'project_id': project_id,
67
+ })
68
+
69
+ res.raise_for_status()
70
+
71
+ def import_body_param_name(project_id: int, endpoint_id: int, body_param_name_resource: BodyParamNamesResource, body_param_name: BodyParamName):
72
+ res: requests.Response = body_param_name_resource.create(endpoint_id, {
73
+ 'name': body_param_name.get('name'),
74
+ 'is_deterministic': body_param_name.get('is_deterministic', True),
75
+ 'is_required': body_param_name.get('is_required', False),
76
+ 'inferred_type': body_param_name.get('inferred_type'),
77
+ 'query': body_param_name.get('query'),
78
+ 'endpoint_id': endpoint_id,
79
+ 'project_id': project_id,
80
+ })
81
+
82
+ res.raise_for_status()
83
+
84
+ def import_query_param_name(project_id: int, endpoint_id: int, query_param_name_resource: QueryParamNamesResource, query_param_name: RequestComponentName):
85
+ res: requests.Response = query_param_name_resource.create(endpoint_id, {
86
+ 'name': query_param_name.get('name'),
87
+ 'is_deterministic': query_param_name.get('is_deterministic', True),
88
+ 'is_required': query_param_name.get('is_required', False),
89
+ 'endpoint_id': endpoint_id,
90
+ 'project_id': project_id,
91
+ })
92
+
93
+ res.raise_for_status()
94
+
95
+ def import_response_param_name(project_id: int, endpoint_id: int, response_param_name_resource: ResponseParamNamesResource, response_param_name: RequestComponentName):
96
+ res: requests.Response = response_param_name_resource.create(endpoint_id, {
97
+ 'name': response_param_name.get('name'),
98
+ 'is_deterministic': response_param_name.get('is_deterministic', True),
99
+ 'is_required': response_param_name.get('is_required', False),
100
+ 'inferred_type': response_param_name.get('inferred_type'),
101
+ 'query': response_param_name.get('query'),
102
+ 'endpoint_id': endpoint_id,
103
+ 'project_id': project_id,
104
+ })
105
+
106
+ res.raise_for_status()
107
+
108
+ def import_response_header_name(project_id: int, endpoint_id: int, response_header_name_resource: ResponseHeaderNamesResource, response_header_name: RequestComponentName):
109
+ res: requests.Response = response_header_name_resource.create(endpoint_id, {
110
+ 'name': response_header_name.get('name'),
111
+ 'is_deterministic': response_header_name.get('is_deterministic', True),
112
+ 'is_required': response_header_name.get('is_required', False),
113
+ 'endpoint_id': endpoint_id,
114
+ 'project_id': project_id,
115
+ })
116
+
117
+ res.raise_for_status()
118
+
119
+ component_name_import_dispatch = {
120
+ ENDPOINT_COMPONENT_NAMES[0]: import_header_name,
121
+ ENDPOINT_COMPONENT_NAMES[1]: import_body_param_name,
122
+ ENDPOINT_COMPONENT_NAMES[2]: import_query_param_name,
123
+ ENDPOINT_COMPONENT_NAMES[3]: import_response_header_name,
124
+ ENDPOINT_COMPONENT_NAMES[4]: import_response_param_name
125
+ }
126
+
127
+ def process_import(component_name: str, *args):
128
+ return component_name_import_dispatch[component_name](*args)
129
+
130
+ def import_component_names(project_id: int, endpoint_id: int, endpoint: EndpointShowResponse, resources: Dict[str, EndpointsResource]):
131
+ for component_name in ENDPOINT_COMPONENT_NAMES:
132
+ for component in endpoint.get(f'{component_name}s', {}):
133
+ resource = resources[component_name]
134
+ process_import(component_name, project_id, endpoint_id, resource, component)
135
+
136
+ def cleanup_endpoint(project_id: int, endpoint_id: int, resource: EndpointsResource):
137
+ print(f"{bcolors.OKBLUE}Cleaning up partial import...{bcolors.ENDC}")
138
+ res: requests.Response = resource.destroy(endpoint_id, **{
139
+ 'project_id': project_id
140
+ })
141
+
142
+ res.raise_for_status()
143
+
144
+ def error_handler(error: requests.HTTPError, message = ''):
145
+ if message:
146
+ print(
147
+ f"{bcolors.FAIL}{message}{bcolors.ENDC}",
148
+ file=sys.stderr
149
+ )
150
+
151
+ print(error.response.text, file=sys.stderr)
152
+ print(f"Error {error.response.status_code} {error.response.reason}", file=sys.stderr)
@@ -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)
@@ -17,7 +17,9 @@ class ReplayCliOptions(TypedDict):
17
17
  lifecycle_hooks_path: str
18
18
  on_response: Callable
19
19
  project_key: str
20
+ public_directory_path: str
20
21
  record: bool
22
+ response_fixtures_path: str
21
23
  scenario_key: str
22
24
  scheme: str
23
25
  trace: Trace
@@ -57,7 +59,9 @@ class ReplayFacade():
57
59
  'host': cli_options.get('host'),
58
60
  'lifecycle_hooks_path': cli_options.get('lifecycle_hooks_path'),
59
61
  'overwrite': cli_options.get('overwrite'),
62
+ 'public_directory_path': cli_options.get('public_directory_path'),
60
63
  'request_origin': request_origin.CLI,
64
+ 'response_fixtures_path': cli_options.get('response_fixtures_path'),
61
65
  'save': cli_options.get('save'),
62
66
  'scheme': cli_options.get('scheme'),
63
67
  'trace_context': trace_context,
@@ -134,8 +134,10 @@ if not is_remote:
134
134
  Configure which tests to print. Defaults to {test_output_level.PASSED}.
135
135
  '''
136
136
  )
137
+ @click.option('--public-directory-path', help='Path to public files. Used for mocking requests.')
137
138
  @ConditionalDecorator(lambda f: click.option('--remote-project-key', help='Use remote project for endpoint definitions.')(f), is_remote and is_local)
138
139
  @ConditionalDecorator(lambda f: click.option('--report-key', help='Save to report.')(f), is_remote)
140
+ @click.option('--response-fixtures-path', help='Path to response fixtures yaml. Used for mocking requests.')
139
141
  @ConditionalDecorator(lambda f: click.option('--save', is_flag=True, default=False, help='Saves test results.')(f), is_remote)
140
142
  @click.option('--scheme', type=click.Choice(['http', 'https']), help='Rewrite request scheme.')
141
143
  @click.option('--strategy', default=test_strategy.DIFF, type=click.Choice([test_strategy.CONTRACT, test_strategy.CUSTOM, test_strategy.DIFF, test_strategy.FUZZY]), help='How to test responses.')
@@ -158,8 +158,10 @@ if is_local:
158
158
  Configure which tests to print. Defaults to {test_output_level.PASSED}.
159
159
  '''
160
160
  )
161
+ @click.option('--public-directory-path', help='Path to public files. Used for mocking requests.')
161
162
  @ConditionalDecorator(lambda f: click.option('--remote-project-key', help='Use remote project for endpoint definitions.')(f), is_remote)
162
163
  @ConditionalDecorator(lambda f: click.option('--report-key', help='Save results to report.')(f), is_remote)
164
+ @click.option('--response-fixtures-path', help='Path to response fixtures yaml. Used for mocking requests.')
163
165
  @ConditionalDecorator(lambda f: click.option('--save', is_flag=True, default=False, help='Save results.')(f), is_remote)
164
166
  @click.option('--scheme', help='Rewrite request scheme.')
165
167
  @click.option(
@@ -15,8 +15,10 @@ class TestOptions(TypedDict):
15
15
  lifecycle_hooks_path: str
16
16
  log_level: logger.LogLevel
17
17
  output_level: test_output_level.TestOutputLevel
18
+ public_directory_path: str
18
19
  remote_project_key: str
19
20
  report_key: str
21
+ response_fixtures_path: str
20
22
  save: str
21
23
  scheme: str
22
24
  strategy: test_strategy.TestStrategy
@@ -35,7 +35,7 @@ class RequestBuilder():
35
35
 
36
36
  @property
37
37
  def request_body(self):
38
- return self.__properties.get('request_body')
38
+ return self.__properties.get('request_body') or b''
39
39
 
40
40
  @property
41
41
  def request_headers(self):
@@ -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
@@ -14,6 +14,7 @@ from stoobly_agent.lib.logger import Logger
14
14
 
15
15
  from .constants import custom_response_codes
16
16
  from .mock.context import MockContext
17
+ from .mock.eval_fixtures_service import eval_fixtures
17
18
  from .mock.eval_request_service import inject_eval_request
18
19
  from .utils.allowed_request_service import get_active_mode_policy
19
20
  from .utils.request_handler import reverse_proxy
@@ -65,14 +66,14 @@ def handle_request_mock_generic(context: MockContext, **options: MockOptions):
65
66
  if handle_failure:
66
67
  res = handle_failure(context)
67
68
  elif policy == mock_policy.ALL:
68
- res = eval_request_with_retry(eval_request, request, **options)
69
+ res = eval_request_with_retry(context, eval_request, **options)
69
70
 
70
71
  context.with_response(res)
71
72
 
72
73
  if handle_success:
73
74
  res = handle_success(context) or res
74
75
  elif policy == mock_policy.FOUND:
75
- res = eval_request_with_retry(eval_request, request, **options)
76
+ res = eval_request_with_retry(context, eval_request, **options)
76
77
 
77
78
  context.with_response(res)
78
79
 
@@ -93,7 +94,8 @@ def handle_request_mock_generic(context: MockContext, **options: MockOptions):
93
94
 
94
95
  return pass_on(context.flow, res)
95
96
 
96
- def eval_request_with_retry(eval_request, request, **options: MockOptions):
97
+ def eval_request_with_retry(context: MockContext, eval_request, **options: MockOptions):
98
+ request = context.flow.request
97
99
  infer = bool(options.get('infer'))
98
100
  ignored_components = options['ignored_components'] if 'ignored_components' in options else []
99
101
 
@@ -103,6 +105,16 @@ def eval_request_with_retry(eval_request, request, **options: MockOptions):
103
105
  ignored_components.append(res.content)
104
106
  res = eval_request(request, ignored_components, infer=infer, retry=1)
105
107
 
108
+ if res.status_code == custom_response_codes.NOT_FOUND:
109
+ intercept_settings = context.intercept_settings
110
+ fixture = eval_fixtures(
111
+ request,
112
+ public_directory_path=intercept_settings.public_directory_path,
113
+ response_fixtures=intercept_settings.response_fixtures
114
+ )
115
+ if fixture:
116
+ res = fixture
117
+
106
118
  return res
107
119
 
108
120
  def handle_request_mock(context: MockContext):