stoobly-agent 1.5.0__py3-none-any.whl → 1.5.2__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 (27) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/cli/helpers/handle_config_update_service.py +2 -2
  3. stoobly_agent/app/cli/scaffold/constants.py +1 -0
  4. stoobly_agent/app/cli/scaffold/templates/app/.Makefile +7 -10
  5. stoobly_agent/app/cli/scaffold/templates/app/.docker-compose.base.yml +1 -0
  6. stoobly_agent/app/cli/scaffold/workflow_run_command.py +7 -1
  7. stoobly_agent/app/cli/scaffold_cli.py +10 -9
  8. stoobly_agent/app/models/factories/resource/local_db/helpers/tiebreak_scenario_request.py +4 -4
  9. stoobly_agent/app/models/factories/resource/local_db/request_adapter.py +17 -11
  10. stoobly_agent/app/models/types/request.py +1 -2
  11. stoobly_agent/app/proxy/handle_mock_service.py +75 -55
  12. stoobly_agent/app/proxy/handle_test_service.py +17 -8
  13. stoobly_agent/app/proxy/mock/custom_not_found_response_builder.py +5 -0
  14. stoobly_agent/app/proxy/mock/eval_fixtures_service.py +3 -2
  15. stoobly_agent/app/proxy/mock/eval_request_service.py +16 -11
  16. stoobly_agent/app/proxy/utils/response_handler.py +11 -0
  17. stoobly_agent/config/constants/custom_headers.py +1 -0
  18. stoobly_agent/config/constants/query_params.py +2 -0
  19. stoobly_agent/test/app/models/factories/resource/local_db/helpers/tiebreak_scenario_request_test.py +4 -4
  20. stoobly_agent/test/cli/mock_scenario_lifecycle_hooks.py +5 -0
  21. stoobly_agent/test/cli/mock_scenario_test.py +62 -0
  22. stoobly_agent/test/cli/mock_test.py +1 -9
  23. {stoobly_agent-1.5.0.dist-info → stoobly_agent-1.5.2.dist-info}/METADATA +1 -1
  24. {stoobly_agent-1.5.0.dist-info → stoobly_agent-1.5.2.dist-info}/RECORD +27 -24
  25. {stoobly_agent-1.5.0.dist-info → stoobly_agent-1.5.2.dist-info}/LICENSE +0 -0
  26. {stoobly_agent-1.5.0.dist-info → stoobly_agent-1.5.2.dist-info}/WHEEL +0 -0
  27. {stoobly_agent-1.5.0.dist-info → stoobly_agent-1.5.2.dist-info}/entry_points.txt +0 -0
stoobly_agent/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
1
  COMMAND = 'stoobly-agent'
2
- VERSION = '1.5.0'
2
+ VERSION = '1.5.2'
@@ -58,9 +58,9 @@ def handle_intercept_active_update(new_settings: Settings, context: Context = No
58
58
  scenario_model.update(_scenario_key.id, **{ 'overwritable': False })[1]
59
59
  elif _mode == intercept_mode.MOCK:
60
60
  # When mock is stopped, clear request access counts
61
- from stoobly_agent.app.models.factories.resource.local_db.helpers.tiebreak_scenario_request import reset
61
+ from stoobly_agent.app.models.factories.resource.local_db.helpers.tiebreak_scenario_request import reset_sessions
62
62
 
63
- reset()
63
+ reset_sessions()
64
64
 
65
65
  def handle_scenario_update(new_settings: Settings, context = None):
66
66
  new_scenario_key = __scenario_key(new_settings.proxy)
@@ -4,6 +4,7 @@ from typing import Literal
4
4
 
5
5
  from stoobly_agent.config.data_dir import CERTS_DIR_NAME, DATA_DIR_NAME
6
6
 
7
+ APP_DIR_ENV = 'APP_DIR'
7
8
  APP_NETWORK_ENV = 'APP_NETWORK'
8
9
  APP_NAME_ENV = 'APP_NAME'
9
10
  BIN_FOLDER_NAME = 'bin'
@@ -20,17 +20,15 @@ ca_certs_dir=$$(realpath "$${STOOBLY_CA_CERTS_DIR:-$$(realpath ~)/.mitmproxy}")
20
20
  certs_dir=$$(realpath "$${STOOBLY_CERTS_DIR:-$(app_data_dir)/certs}")
21
21
  context_dir=$$(realpath "$${STOOBLY_CONTEXT_DIR:-$(CONTEXT_DIR_DEFAULT)}")
22
22
 
23
- context_dir_option=--context-dir-path $(context_dir)
24
23
  user_id_option=--user-id $(USER_ID)
25
24
  stoobly_exec_options=--profile $(EXEC_WORKFLOW_NAME) -p $(EXEC_WORKFLOW_NAME)
26
25
  workflow_down_options=$(user_id_option)
27
26
  workflow_service_options=$(shell echo $$STOOBLY_WORKFLOW_SERVICE_OPTIONS)
28
- workflow_up_options=$(context_dir_option) --ca-certs-dir-path $(ca_certs_dir) --certs-dir-path $(certs_dir) --from-make $(user_id_option)
27
+ workflow_up_options=--app-dir-path $(app_dir) --context-dir-path $(context_dir) --ca-certs-dir-path $(ca_certs_dir) --certs-dir-path $(certs_dir) --from-make $(user_id_option)
29
28
 
30
29
  app_data_dir=$(app_dir)/.stoobly
31
30
  app_namespace_dir=$(app_data_dir)/docker
32
31
  app_tmp_dir=$(app_data_dir)/tmp
33
- data_dir=$(context_dir)/.stoobly
34
32
  dockerfile_path=$(app_namespace_dir)/.Dockerfile.context
35
33
  docker_compose_file_path=$(app_namespace_dir)/stoobly-ui/exec/.docker-compose.exec.yml
36
34
  workflow_run_script=$(app_data_dir)/tmp/run.sh
@@ -38,7 +36,7 @@ workflow_run_script=$(app_data_dir)/tmp/run.sh
38
36
  # Commands
39
37
  docker_command=docker
40
38
  docker_compose_command=$(docker_command) compose
41
- exec_env=export CA_CERTS_DIR="$(ca_certs_dir)" && export USER_ID=$(USER_ID)
39
+ exec_env=APP_DIR="$(app_dir)" CA_CERTS_DIR="$(ca_certs_dir)" USER_ID="$(USER_ID)"
42
40
  exec_up=$(docker_compose_command) -f "$(docker_compose_file_path)" $(stoobly_exec_options) up --remove-orphans
43
41
  source_env=set -a; [ -f .env ] && source .env; set +a
44
42
 
@@ -47,15 +45,14 @@ stoobly_exec_build=$(docker_command) build $(stoobly_exec_build_args) $(app_name
47
45
  stoobly_exec_build_args=-f "$(dockerfile_path)" -t stoobly.$(USER_ID) --build-arg USER_ID=$(USER_ID) $(PULL_OPTION) --quiet
48
46
 
49
47
  # Exec any
50
- stoobly_exec=$(stoobly_exec_build) && $(stoobly_exec_env) && $(exec_up)
51
- stoobly_exec_env=$(source_env) && $(exec_env) && export CONTEXT_DIR="$(context_dir)"
48
+ stoobly_exec=$(stoobly_exec_build) && $(stoobly_exec_env) $(exec_up)
49
+ stoobly_exec_env=$(source_env) && $(exec_env) CONTEXT_DIR="$(context_dir)"
52
50
 
53
51
  # Exec workflow run
54
52
  # Because scaffold is stored in the application source code directory,
55
- # when running a scaffold command from within a container,
56
- # it needs access to $(app_dir) rather than $(context_dir)
57
- stoobly_exec_run=$(stoobly_exec_build) && $(stoobly_exec_run_env) && $(exec_up)
58
- stoobly_exec_run_env=$(source_env) && $(exec_env) && export CONTEXT_DIR="$(app_dir)"
53
+ # when running a scaffold command from within a container, it needs access to $(app_dir) rather than $(context_dir)
54
+ stoobly_exec_run=$(stoobly_exec_build) && $(stoobly_exec_run_env) $(exec_up)
55
+ stoobly_exec_run_env=$(source_env) && $(exec_env) CONTEXT_DIR="$(app_dir)"
59
56
 
60
57
  # Workflow run
61
58
  workflow_run=$(source_env) && bash "$(workflow_run_script)"
@@ -4,6 +4,7 @@ services:
4
4
  service: stoobly_base
5
5
  volumes:
6
6
  - ${CONTEXT_DIR}/.stoobly:/home/stoobly/.stoobly
7
+ - ${APP_DIR}/.stoobly/docker:/home/stoobly/.stoobly/docker
7
8
  stoobly_base:
8
9
  image: stoobly.${USER_ID}
9
10
  ui_base:
@@ -11,7 +11,7 @@ from stoobly_agent.lib.logger import Logger
11
11
 
12
12
  from .app import App
13
13
  from .constants import (
14
- APP_NETWORK_ENV, CA_CERTS_DIR_ENV, CERTS_DIR_ENV, CONTEXT_DIR_ENV, NAMESERVERS_FILE,
14
+ APP_DIR_ENV, APP_NETWORK_ENV, CA_CERTS_DIR_ENV, CERTS_DIR_ENV, CONTEXT_DIR_ENV, NAMESERVERS_FILE,
15
15
  SERVICE_DNS_ENV, SERVICE_NAME_ENV, USER_ID_ENV, WORKFLOW_NAME_ENV, WORKFLOW_NAMESPACE_ENV
16
16
  )
17
17
  from .docker.constants import DOCKERFILE_CONTEXT
@@ -41,12 +41,17 @@ class WorkflowRunCommand(WorkflowCommand):
41
41
  def __init__(self, app: App, **kwargs):
42
42
  super().__init__(app, **kwargs)
43
43
 
44
+ self.__app_dir_path = os.path.abspath(kwargs.get('app_dir_path') or app.dir_path)
44
45
  self.__current_working_dir = os.getcwd()
45
46
  self.__ca_certs_dir_path = kwargs.get('ca_certs_dir_path') or app.ca_certs_dir_path
46
47
  self.__certs_dir_path = kwargs.get('certs_dir_path') or app.certs_dir_path
47
48
  self.__context_dir_path = kwargs.get('context_dir_path') or app.context_dir_path
48
49
  self.__network = kwargs.get('network') or self.app_config.network
49
50
 
51
+ @property
52
+ def app_dir_path(self):
53
+ return self.__app_dir_path
54
+
50
55
  @property
51
56
  def ca_certs_dir_path(self):
52
57
  return self.__ca_certs_dir_path
@@ -244,6 +249,7 @@ class WorkflowRunCommand(WorkflowCommand):
244
249
  user_id = options.get('user_id')
245
250
 
246
251
  _config = {}
252
+ _config[APP_DIR_ENV] = self.app_dir_path
247
253
  _config[CA_CERTS_DIR_ENV] = self.ca_certs_dir_path
248
254
  _config[CERTS_DIR_ENV] = self.certs_dir_path
249
255
  _config[CONTEXT_DIR_ENV] = self.context_dir_path
@@ -170,7 +170,7 @@ def create(**kwargs):
170
170
  help="List services",
171
171
  name="list"
172
172
  )
173
- @click.option('--app-dir-path', default=os.getcwd(), help='Path to application directory.')
173
+ @click.option('--app-dir-path', default=current_working_dir, help='Path to application directory.')
174
174
  @click.option('--format', type=click.Choice(FORMATS), help='Format output.')
175
175
  @click.option('--select', multiple=True, help='Select column(s) to display.')
176
176
  @click.option('--service', multiple=True, help='Select specific services.')
@@ -302,8 +302,6 @@ def copy(**kwargs):
302
302
  @click.option('--user-id', default=os.getuid(), help='OS user ID of the owner of context dir path.')
303
303
  @click.argument('workflow_name')
304
304
  def down(**kwargs):
305
- cwd = os.getcwd()
306
-
307
305
  os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
308
306
 
309
307
  app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
@@ -322,7 +320,7 @@ def down(**kwargs):
322
320
  config = { **kwargs }
323
321
  config['service_name'] = service
324
322
  command = WorkflowRunCommand(app, **config)
325
- command.current_working_dir = cwd
323
+ command.current_working_dir = current_working_dir
326
324
  commands.append(command)
327
325
 
328
326
  commands = sorted(commands, key=lambda command: command.service_config.priority)
@@ -445,11 +443,14 @@ def logs(**kwargs):
445
443
  @click.option('--verbose', is_flag=True)
446
444
  @click.argument('workflow_name')
447
445
  def up(**kwargs):
448
- cwd = os.getcwd()
449
-
450
446
  os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
451
447
 
452
- app = App(kwargs['app_dir_path'], DOCKER_NAMESPACE, **kwargs)
448
+ from_make = kwargs['from_make']
449
+
450
+ # Because we are running a docker-compose command which depends on APP_DIR env var
451
+ # when we are running this command through make, the host's app_dir_path will likely differ
452
+ app_dir_path = current_working_dir if from_make else kwargs['app_dir_path']
453
+ app = App(app_dir_path, DOCKER_NAMESPACE, **kwargs)
453
454
  __validate_app(app)
454
455
 
455
456
  # If namespace is set, default network to namespace
@@ -469,7 +470,7 @@ def up(**kwargs):
469
470
  config = { **kwargs }
470
471
  config['service_name'] = service
471
472
  command = WorkflowRunCommand(app, **config)
472
- command.current_working_dir = cwd
473
+ command.current_working_dir = current_working_dir
473
474
  commands.append(command)
474
475
 
475
476
  # Before services can be started, their image and network needs to be created
@@ -477,7 +478,7 @@ def up(**kwargs):
477
478
  command: WorkflowRunCommand = commands[0]
478
479
 
479
480
  init_commands = []
480
- if not kwargs['from_make']:
481
+ if not from_make:
481
482
  create_image_command = command.create_image(user_id=kwargs['user_id'], verbose=kwargs['verbose'])
482
483
  init_commands.append(create_image_command)
483
484
 
@@ -5,7 +5,7 @@ from typing import List
5
5
  from stoobly_agent.lib.cache import Cache
6
6
  from stoobly_agent.lib.orm.request import Request
7
7
 
8
- PREFIX = 'last_request_id'
8
+ SUFFIX = 'last_request_id'
9
9
 
10
10
  def access_request(session_id: str, request_id: int, timeout = None):
11
11
  cache = Cache.instance()
@@ -20,8 +20,8 @@ def generate_session_id(query: dict):
20
20
 
21
21
  return hashlib.md5(b'.'.join(toks)).hexdigest()
22
22
 
23
- def reset():
24
- Cache.instance().clear(f".+\.{PREFIX}")
23
+ def reset_sessions():
24
+ Cache.instance().clear(f".+\\.{SUFFIX}")
25
25
 
26
26
  def tiebreak_scenario_request(session_id: str, requests: List[Request]):
27
27
  if len(requests) == 0:
@@ -48,4 +48,4 @@ def tiebreak_scenario_request(session_id: str, requests: List[Request]):
48
48
 
49
49
  def __last_request_id_key(_key = None):
50
50
  _key = _key or generate_session_id()
51
- return f"{_key}.{PREFIX}"
51
+ return f"{_key}.{SUFFIX}"
@@ -10,7 +10,7 @@ from stoobly_agent.app.models.types import RequestCreateParams, RequestDestroyPa
10
10
  from stoobly_agent.app.proxy.mock.custom_not_found_response_builder import CustomNotFoundResponseBuilder
11
11
  from stoobly_agent.app.proxy.mock.ignored_components_response_builder import IgnoreComponentsResponseBuilder
12
12
  from stoobly_agent.app.proxy.record.joined_request import JoinedRequest
13
- from stoobly_agent.config.constants import custom_headers
13
+ from stoobly_agent.config.constants import custom_headers, query_params as request_query_params
14
14
  from stoobly_agent.lib.orm import ORM
15
15
  from stoobly_agent.lib.orm.request import Request
16
16
  from stoobly_agent.lib.orm.response import Response
@@ -73,7 +73,6 @@ class LocalDBRequestAdapter(LocalDBAdapter):
73
73
  def response(self, **query_params: RequestColumns) -> requests.Response:
74
74
  self.__adapt_scenario_id(query_params)
75
75
 
76
- endpoint_promise = query_params.get('endpoint_promise')
77
76
  request = None
78
77
 
79
78
  if not query_params.get('request_id'):
@@ -85,16 +84,19 @@ class LocalDBRequestAdapter(LocalDBAdapter):
85
84
  requests = self.__request_orm.where_for(**request_columns).get()
86
85
 
87
86
  if 'scenario_id' in query_params:
88
- # TODO: Would need an additional ID to distinguish different scenario sessions
89
- session_id = generate_session_id(request_columns)
90
-
91
87
  if len(requests) > 1:
92
- request = tiebreak_scenario_request(session_id, requests)
88
+ session_id = query_params.get(request_query_params.SESSION_ID)
89
+ request_session_id_components = { **request_columns }
90
+
91
+ if session_id:
92
+ request_session_id_components[request_query_params.SESSION_ID] = session_id
93
+
94
+ # When multiple requests are matched for a scenario, return them in sequence
95
+ request_session_id = generate_session_id(request_session_id_components)
96
+ request = tiebreak_scenario_request(request_session_id, requests)
97
+ access_request(request_session_id, request.id)
93
98
  else:
94
99
  request = requests.last()
95
-
96
- if request:
97
- access_request(session_id, request.id)
98
100
  else:
99
101
  request = requests.last()
100
102
  else:
@@ -104,6 +106,7 @@ class LocalDBRequestAdapter(LocalDBAdapter):
104
106
  request = None
105
107
 
106
108
  if not request:
109
+ endpoint_promise = query_params.get(request_query_params.ENDPOINT_PROMISE)
107
110
  return self.__handle_request_not_found(endpoint_promise)
108
111
 
109
112
  response_record = request.response
@@ -315,8 +318,8 @@ class LocalDBRequestAdapter(LocalDBAdapter):
315
318
  return candidates.get()
316
319
 
317
320
  def __filter_request_response_columns(self, request_columns: RequestCreateParams):
318
- if request_columns.get('endpoint_promise'):
319
- del request_columns['endpoint_promise']
321
+ if request_columns.get(request_query_params.ENDPOINT_PROMISE):
322
+ del request_columns[request_query_params.ENDPOINT_PROMISE]
320
323
 
321
324
  if request_columns.get('infer'):
322
325
  del request_columns['infer']
@@ -327,6 +330,9 @@ class LocalDBRequestAdapter(LocalDBAdapter):
327
330
  if request_columns.get('retry'):
328
331
  del request_columns['retry']
329
332
 
333
+ if request_columns.get(request_query_params.SESSION_ID):
334
+ del request_columns[request_query_params.SESSION_ID]
335
+
330
336
  def __request(self, request_id: str):
331
337
  if self.validate_uuid(request_id):
332
338
  return self.__request_orm.find_by(uuid=request_id)
@@ -29,5 +29,4 @@ class RequestShowParams(TypedDict):
29
29
  headers: bool
30
30
  project_id: str
31
31
  query_params: bool
32
- response: bool
33
-
32
+ response: bool
@@ -3,7 +3,7 @@ import pdb
3
3
  import requests
4
4
  import time
5
5
 
6
- from mitmproxy.http import Request as MitmproxyRequest
6
+ from mitmproxy.http import HTTPFlow as MitmproxyHTTPFlow, Request as MitmproxyRequest
7
7
  from typing import Callable, TypedDict
8
8
 
9
9
  from stoobly_agent.app.models.request_model import RequestModel
@@ -18,7 +18,7 @@ from .mock.eval_fixtures_service import eval_fixtures
18
18
  from .mock.eval_request_service import inject_eval_request
19
19
  from .utils.allowed_request_service import get_active_mode_policy
20
20
  from .utils.request_handler import reverse_proxy
21
- from .utils.response_handler import bad_request, pass_on
21
+ from .utils.response_handler import bad_request, enable_cors, pass_on
22
22
  from .utils.rewrite import rewrite_request, rewrite_response
23
23
 
24
24
  LOG_ID = 'Mock'
@@ -44,60 +44,68 @@ def handle_request_mock_generic_without_rewrite(context: MockContext, **options:
44
44
  # @param settings [Dict]
45
45
  #
46
46
  def handle_request_mock_generic(context: MockContext, **options: MockOptions):
47
+ handle_error = options['error'] if 'error' in options and callable(options['error']) else None
48
+ handle_failure = options['failure'] if 'failure' in options and callable(options['failure']) else None
49
+ handle_success = options['success'] if 'success' in options and callable(options['success']) else None
47
50
  intercept_settings = context.intercept_settings
48
51
  request: MitmproxyRequest = context.flow.request
49
- handle_success = options['success'] if 'success' in options and callable(options['success']) else None
50
- handle_failure = options['failure'] if 'failure' in options and callable(options['failure']) else None
52
+ res = None
51
53
 
52
54
  policy = get_active_mode_policy(request, intercept_settings)
53
55
  if policy == mock_policy.NONE:
56
+ if handle_error:
57
+ res = handle_error(context)
58
+
59
+ return pass_on(context.flow, res)
60
+
61
+ if policy not in [mock_policy.ALL, mock_policy.FOUND]:
62
+ if handle_error:
63
+ res = handle_error(context)
64
+
65
+ return bad_request(
66
+ context.flow,
67
+ "Valid env MOCK_POLICY: %s, Got: %s" %
68
+ ([mock_policy.ALL, mock_policy.FOUND, mock_policy.NONE], policy)
69
+ )
70
+
71
+ if not options.get('no_rewrite'):
72
+ __rewrite_request(context)
73
+
74
+ __mock_hook(lifecycle_hooks.BEFORE_MOCK, context)
75
+
76
+ # If ignore rules are set, then ignore specified request parameters
77
+ ignore_rules = intercept_settings.ignore_rules
78
+ if len(ignore_rules) > 0:
79
+ request_facade = MitmproxyRequestFacade(request)
80
+ _ignore_rules = request_facade.select_parameter_rules(ignore_rules)
81
+ ignored_components = rewrite_rules_to_ignored_components(_ignore_rules)
82
+ options['ignored_components'] += ignored_components if 'ignored_components' in options else ignored_components
83
+
84
+ request_model = RequestModel(intercept_settings.settings)
85
+ eval_request = inject_eval_request(request_model, intercept_settings)
86
+
87
+ if policy == mock_policy.ALL:
88
+ res = eval_request_with_retry(context, eval_request, **options)
89
+
90
+ context.with_response(res)
91
+ elif policy == mock_policy.FOUND:
92
+ res = eval_request_with_retry(context, eval_request, **options)
93
+
94
+ context.with_response(res)
95
+
96
+ if res.status_code in [custom_response_codes.NOT_FOUND, custom_response_codes.IGNORE_COMPONENTS]:
97
+ try:
98
+ res = __handle_found_policy(context)
99
+ except RuntimeError:
100
+ # Do nothing, return custom error response
101
+ pass
102
+
103
+ if res.status_code == custom_response_codes.NOT_FOUND:
54
104
  if handle_failure:
55
- res = handle_failure(context)
105
+ res = handle_failure(context) or res
56
106
  else:
57
- if not options.get('no_rewrite'):
58
- __rewrite_request(context)
59
-
60
- __mock_hook(lifecycle_hooks.BEFORE_MOCK, context)
61
-
62
- # If ignore rules are set, then ignore specified request parameters
63
- ignore_rules = intercept_settings.ignore_rules
64
- if len(ignore_rules) > 0:
65
- request_facade = MitmproxyRequestFacade(request)
66
- _ignore_rules = request_facade.select_parameter_rules(ignore_rules)
67
- ignored_components = rewrite_rules_to_ignored_components(_ignore_rules)
68
- options['ignored_components'] += ignored_components if 'ignored_components' in options else ignored_components
69
-
70
- request_model = RequestModel(intercept_settings.settings)
71
- eval_request = inject_eval_request(request_model, intercept_settings)
72
-
73
- if policy == mock_policy.ALL:
74
- res = eval_request_with_retry(context, eval_request, **options)
75
-
76
- context.with_response(res)
77
-
78
- if handle_success:
79
- res = handle_success(context) or res
80
- elif policy == mock_policy.FOUND:
81
- res = eval_request_with_retry(context, eval_request, **options)
82
-
83
- context.with_response(res)
84
-
85
- if res.status_code in [custom_response_codes.NOT_FOUND, custom_response_codes.IGNORE_COMPONENTS]:
86
- if handle_failure:
87
- try:
88
- res = handle_failure(context)
89
- except RuntimeError:
90
- # Do nothing, return custom error response
91
- pass
92
- else:
93
- if handle_success:
94
- res = handle_success(context) or res
95
- else:
96
- return bad_request(
97
- context.flow,
98
- "Valid env MOCK_POLICY: %s, Got: %s" %
99
- ([mock_policy.ALL, mock_policy.FOUND, mock_policy.NONE], policy)
100
- )
107
+ if handle_success:
108
+ res = handle_success(context) or res
101
109
 
102
110
  return pass_on(context.flow, res)
103
111
 
@@ -147,13 +155,19 @@ def handle_response_mock(context: MockContext):
147
155
  __rewrite_response(context)
148
156
  __mock_hook(lifecycle_hooks.AFTER_MOCK, context)
149
157
 
150
- def __handle_mock_success(context: MockContext) -> None:
151
- if os.environ.get(env_vars.AGENT_SIMULATE_LATENCY):
152
- response = context.response
153
- start_time = context.start_time
154
- __simulate_latency(response.headers.get(custom_headers.RESPONSE_LATENCY), start_time)
155
-
156
158
  def __handle_mock_failure(context: MockContext) -> None:
159
+ flow = context.flow
160
+ request = flow.request
161
+
162
+ if request.method.upper() != 'OPTIONS':
163
+ return False
164
+
165
+ # Default OPTIONS request to allow CORS
166
+ enable_cors(flow)
167
+
168
+ return True
169
+
170
+ def __handle_found_policy(context: MockContext) -> None:
157
171
  req = context.flow.request
158
172
  intercept_settings = context.intercept_settings
159
173
  upstream_url = intercept_settings.upstream_url
@@ -169,6 +183,12 @@ def __handle_mock_failure(context: MockContext) -> None:
169
183
 
170
184
  reverse_proxy(req, upstream_url, {})
171
185
 
186
+ def __handle_mock_success(context: MockContext) -> None:
187
+ if os.environ.get(env_vars.AGENT_SIMULATE_LATENCY):
188
+ response = context.response
189
+ start_time = context.start_time
190
+ __simulate_latency(response.headers.get(custom_headers.RESPONSE_LATENCY), start_time)
191
+
172
192
  def __rewrite_request(context: MockContext):
173
193
  # Rewrite request with paramter rules for mock
174
194
 
@@ -62,6 +62,7 @@ def handle_response_test(context: ReplayContext) -> None:
62
62
  # At this point, the request may already been rewritten during replay, do not rewrite again
63
63
  handle_request_mock_generic_without_rewrite(
64
64
  MockContext(flow, intercept_settings),
65
+ error=lambda mock_context: __handle_mock_error(TestContext(context, mock_context)),
65
66
  failure=lambda mock_context: __handle_mock_failure(TestContext(context, mock_context)),
66
67
  #infer=intercept_settings.test_strategy == test_strategy.FUZZY, # For fuzzy testing we can use an inferred response
67
68
  success=lambda mock_context: __handle_mock_success(TestContext(context, mock_context))
@@ -71,6 +72,22 @@ def __decorate_test_id(flow: MitmproxyHTTPFlow, test_response: TestShowResponse)
71
72
  if test_response.get('id'):
72
73
  flow.response.headers[custom_headers.TEST_ID] = str(test_response['id'])
73
74
 
75
+ def __handle_mock_error(test_context: TestContext):
76
+ Logger.instance().warn(f"{LOG_ID}:TestStatus: Mock not enabled")
77
+
78
+ intercept_settings = test_context.intercept_settings
79
+
80
+ if intercept_settings.request_origin == request_origin.CLI:
81
+ return build_response(False, 'No test found')
82
+
83
+ def __handle_mock_failure(test_context: TestContext) -> None:
84
+ Logger.instance().warn(f"{LOG_ID}:TestStatus: No test found")
85
+
86
+ intercept_settings = test_context.intercept_settings
87
+
88
+ if intercept_settings.request_origin == request_origin.CLI:
89
+ return build_response(False, 'No test found')
90
+
74
91
  def __handle_mock_success(test_context: TestContext) -> None:
75
92
  flow: MitmproxyHTTPFlow = test_context.flow
76
93
 
@@ -138,14 +155,6 @@ def __handle_mock_success(test_context: TestContext) -> None:
138
155
 
139
156
  return flow.response
140
157
 
141
- def __handle_mock_failure(test_context: TestContext) -> None:
142
- Logger.instance().warn(f"{LOG_ID}:TestStatus: No test found")
143
-
144
- intercept_settings = test_context.intercept_settings
145
-
146
- if intercept_settings.request_origin == request_origin.CLI:
147
- return build_response(False, 'No test found')
148
-
149
158
  def __override_response(flow: MitmproxyHTTPFlow, content: bytes):
150
159
  headers = { 'Content-Type': 'text/plain' }
151
160
  headers[custom_headers.CONTENT_TYPE] = custom_headers.CONTENT_TYPE_TEST_RESULTS
@@ -12,4 +12,9 @@ class CustomNotFoundResponseBuilder():
12
12
  def build(self):
13
13
  self.__response.status_code = custom_response_codes.NOT_FOUND
14
14
  self.__response.raw = BytesIO('Request not found'.encode())
15
+ self.__response.headers = {
16
+ 'Access-Control-Allow-Origin': '*',
17
+ 'Access-Control-Allow-Methods': 'GET, OPTIONS, POST, PATCH, PUT, DELETE',
18
+ 'Content-Type': 'text/plain',
19
+ }
15
20
  return self.__response
@@ -40,8 +40,9 @@ def eval_fixtures(request: MitmproxyRequest, **options: Options) -> Union[Respon
40
40
 
41
41
  if not fixture_path:
42
42
  fixture_path = _fixture_path
43
- if not os.path.isfile(fixture_path):
44
- return
43
+
44
+ if not os.path.isfile(fixture_path):
45
+ return
45
46
  else:
46
47
  fixture_path = fixture.get('path')
47
48
  if not fixture_path or not os.path.isfile(fixture_path):
@@ -5,16 +5,17 @@ import re
5
5
  from mitmproxy.http import Request as MitmproxyRequest
6
6
  from requests import Response
7
7
  from typing import List, TypedDict, Union
8
- from stoobly_agent.config.constants import custom_headers
9
8
 
10
- from stoobly_agent.lib.api.param_builder import ParamBuilder
11
- from stoobly_agent.lib.api.interfaces.requests import RequestResponseShowQueryParams
12
- from stoobly_agent.lib.logger import Logger
13
9
  from stoobly_agent.app.models.request_model import RequestModel
14
10
  from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
11
+ from stoobly_agent.app.proxy.mock.custom_not_found_response_builder import CustomNotFoundResponseBuilder
15
12
  from stoobly_agent.app.settings import Settings
16
13
  from stoobly_agent.app.settings.constants import request_component
17
14
  from stoobly_agent.app.settings.match_rule import MatchRule
15
+ from stoobly_agent.config.constants import custom_headers, query_params as request_query_params
16
+ from stoobly_agent.lib.api.param_builder import ParamBuilder
17
+ from stoobly_agent.lib.api.interfaces.requests import RequestResponseShowQueryParams
18
+ from stoobly_agent.lib.logger import Logger
18
19
 
19
20
  from .hashed_request_decorator import HashedRequestDecorator
20
21
  from .search_endpoint import inject_search_endpoint
@@ -40,10 +41,6 @@ def inject_eval_request(
40
41
  request_model, intercept_settings, request, ignored_components or [], **options
41
42
  )
42
43
 
43
- ###
44
- #
45
- # @param settings [Settings.mode.mock | Settings.mode.record]
46
- #
47
44
  def eval_request(
48
45
  request_model: RequestModel,
49
46
  intercept_settings: InterceptSettings,
@@ -52,9 +49,14 @@ def eval_request(
52
49
  **options: EvalRequestOptions
53
50
  ) -> Response:
54
51
  query_params_builder = ParamBuilder({})
55
- query_params_builder.with_resource_scoping(intercept_settings.project_key, intercept_settings.scenario_key)
56
52
 
57
- # Tease out API returning ignored components on not found
53
+ try:
54
+ query_params_builder.with_resource_scoping(intercept_settings.project_key, intercept_settings.scenario_key)
55
+ except:
56
+ # If project_key or scenario_key are invalid, assume custom not found
57
+ return CustomNotFoundResponseBuilder().build()
58
+
59
+ # Tease out API returning ignored components on custom not found
58
60
  if request_model.is_local and not options.get('retry'):
59
61
  remote_project_key = intercept_settings.parsed_remote_project_key
60
62
 
@@ -63,7 +65,7 @@ def eval_request(
63
65
  remote_project_id = remote_project_key.id
64
66
  endpoint_promise = lambda: search_endpoint(remote_project_id, request.method, request.url, ignored_components=1)
65
67
 
66
- query_params_builder.with_param('endpoint_promise', endpoint_promise)
68
+ query_params_builder.with_param(request_query_params.ENDPOINT_PROMISE, endpoint_promise)
67
69
 
68
70
  ignored_components = __build_ignored_components(ignored_components_list or [])
69
71
  query_params_builder.with_params(__build_request_params(request, ignored_components))
@@ -142,6 +144,9 @@ def __build_optional_params(request: MitmproxyRequest, options: EvalRequestOptio
142
144
  if custom_headers.MOCK_REQUEST_ID in headers:
143
145
  optional_params['request_id'] = headers[custom_headers.MOCK_REQUEST_ID]
144
146
 
147
+ if custom_headers.SESSION_ID in headers:
148
+ optional_params[request_query_params.SESSION_ID] = headers[custom_headers.SESSION_ID]
149
+
145
150
  return optional_params
146
151
 
147
152
  def __filter_by_match_rules(request: MitmproxyRequest, match_rules: List[MatchRule], query_params: RequestResponseShowQueryParams):
@@ -36,6 +36,17 @@ def bad_request(flow: MitmproxyHTTPFlow, message: str):
36
36
  {'Content-Type': 'text/plain'} # (optional) headers
37
37
  )
38
38
 
39
+ def enable_cors(flow: MitmproxyHTTPFlow):
40
+ flow.response = MitmproxyResponse.make(
41
+ 200,
42
+ '',
43
+ {
44
+ 'Access-Control-Allow-Origin': '*',
45
+ 'Access-Control-Allow-Methods': 'GET, OPTIONS, POST, PATCH, PUT, DELETE',
46
+ 'Access-Control-Allow-Headers': '*'
47
+ }
48
+ )
49
+
39
50
  # Without deleting this header, causes parsing issues when reading response
40
51
  def disable_transfer_encoding(response: MitmproxyResponse) -> None:
41
52
  header_name = 'Transfer-Encoding'
@@ -19,6 +19,7 @@ RESPONSE_ID = 'X-Stoobly-Response-Id'
19
19
  RESPONSE_LATENCY = 'X-Stoobly-Request-Response-Latency'
20
20
  RESPONSE_PROXY_MODE = 'X-Stoobly-Response-Proxy-Mode'
21
21
  SCENARIO_KEY = 'X-Stoobly-Scenario-Key'
22
+ SESSION_ID = 'X-Stoobly-Session-Id'
22
23
  SERVICE_URL = 'X-Stoobly-Service-Url'
23
24
  TEST_FILTER = 'X-Stoobly-Test-Filter'
24
25
  TEST_ID = 'X-Stoobly-Test-Id'
@@ -0,0 +1,2 @@
1
+ ENDPOINT_PROMISE = 'endpoint_promise'
2
+ SESSION_ID = 'session_id'
@@ -5,7 +5,7 @@ import time
5
5
  from typing import List
6
6
 
7
7
  from stoobly_agent.app.models.factories.resource.local_db.helpers.tiebreak_scenario_request import (
8
- access_request, generate_session_id, reset as reset_tiebreak, tiebreak_scenario_request
8
+ access_request, generate_session_id, reset_sessions, tiebreak_scenario_request
9
9
  )
10
10
  from stoobly_agent.lib.cache import Cache
11
11
  from stoobly_agent.lib.orm.request import Request
@@ -92,7 +92,7 @@ class TestTiebreakScenarioRequest():
92
92
  request = tiebreak_scenario_request(session_id, requests)
93
93
  assert request.id == 2
94
94
 
95
- class TestWhenReset():
95
+ class TestWhenResetSessions():
96
96
  @pytest.fixture(scope='class')
97
97
  def created_request_one(self):
98
98
  return RequestMock(1)
@@ -105,7 +105,7 @@ class TestTiebreakScenarioRequest():
105
105
  def test_it_resets(self, cache: Cache, created_request_one: Request):
106
106
  cache.write('persists', 1)
107
107
  access_request('1', created_request_one.id)
108
- reset_tiebreak()
108
+ reset_sessions()
109
109
 
110
110
  assert cache.read('persists') != None
111
- assert len(cache.read_all()) == 1
111
+ assert len(cache.read_all()) == 1
@@ -0,0 +1,5 @@
1
+ from stoobly_agent.app.proxy.context import InterceptContext
2
+ from stoobly_agent.config.constants.custom_headers import MOCK_REQUEST_ID
3
+
4
+ def handle_before_response(context: InterceptContext):
5
+ print(context.flow.response.headers[MOCK_REQUEST_ID], end='')
@@ -0,0 +1,62 @@
1
+ import pdb
2
+ import pytest
3
+
4
+ from click.testing import CliRunner
5
+ from pathlib import Path
6
+
7
+ from stoobly_agent.test.test_helper import DETERMINISTIC_GET_REQUEST_URL, NON_DETERMINISTIC_GET_REQUEST_URL
8
+
9
+ from stoobly_agent.config.constants.custom_headers import SESSION_ID
10
+ from stoobly_agent.cli import mock, record, scenario
11
+ from stoobly_agent.lib.api.keys.scenario_key import ScenarioKey
12
+ from stoobly_agent.lib.orm.request import Request
13
+
14
+ @pytest.fixture(scope='module')
15
+ def runner():
16
+ return CliRunner()
17
+
18
+ class TestMocking():
19
+ @pytest.fixture(scope='class')
20
+ def lifecycle_hooks_path(self):
21
+ return str(Path(__file__).parent / 'mock_scenario_lifecycle_hooks.py')
22
+
23
+ class TestScenario():
24
+ @pytest.fixture(scope='class')
25
+ def scenario_key(self, runner: CliRunner):
26
+ res = runner.invoke(scenario, ['create', '--select', 'key', '--without-headers', 'test-scenario'])
27
+ assert res.exit_code == 0
28
+ return ScenarioKey(res.stdout.strip()).raw
29
+
30
+ @pytest.fixture(autouse=True, scope='class')
31
+ def recorded_request1(self, runner: CliRunner, scenario_key: str):
32
+ record_result = runner.invoke(record, ['--scenario-key', scenario_key, DETERMINISTIC_GET_REQUEST_URL])
33
+ assert record_result.exit_code == 0
34
+ return Request.last()
35
+
36
+ @pytest.fixture(autouse=True, scope='class')
37
+ def recorded_request2(self, runner: CliRunner, scenario_key: str):
38
+ record_result = runner.invoke(record, ['--scenario-key', scenario_key, DETERMINISTIC_GET_REQUEST_URL])
39
+ assert record_result.exit_code == 0
40
+ return Request.last()
41
+
42
+ def test_it_does_not_mocks(self, runner: CliRunner, scenario_key: str):
43
+ mock_result = runner.invoke(mock, ['--scenario-key', scenario_key, NON_DETERMINISTIC_GET_REQUEST_URL])
44
+ assert mock_result.exit_code == 1
45
+
46
+ def test_it_mocks(self, runner: CliRunner, scenario_key: str):
47
+ mock_result = runner.invoke(mock, ['--scenario-key', scenario_key, DETERMINISTIC_GET_REQUEST_URL])
48
+ assert mock_result.exit_code == 0
49
+
50
+ def test_it_mocks_in_order(
51
+ self, runner: CliRunner, lifecycle_hooks_path: str, recorded_request1: Request, recorded_request2: Request, scenario_key: str
52
+ ):
53
+ session_id = 'test'
54
+ args = [
55
+ '--lifecycle-hooks-path', lifecycle_hooks_path, '--scenario-key', scenario_key, '--output', '/dev/null',
56
+ '-H', f"{SESSION_ID}: {session_id}", DETERMINISTIC_GET_REQUEST_URL
57
+ ]
58
+ mock_result = runner.invoke(mock, args)
59
+ assert int(mock_result.stdout) == recorded_request1.id
60
+
61
+ mock_result = runner.invoke(mock, args)
62
+ assert int(mock_result.stdout) == recorded_request2.id
@@ -8,8 +8,7 @@ from stoobly_agent.test.test_helper import DETERMINISTIC_GET_REQUEST_URL, reset
8
8
 
9
9
  from stoobly_agent.config.constants import mode
10
10
  from stoobly_agent.app.settings.constants import request_component
11
- from stoobly_agent.cli import config, mock, record, scenario
12
- from stoobly_agent.lib.api.keys.scenario_key import ScenarioKey
11
+ from stoobly_agent.cli import config, mock, record
13
12
  from stoobly_agent.lib.orm.request import Request
14
13
 
15
14
  @pytest.fixture(scope='module')
@@ -143,10 +142,3 @@ class TestMocking():
143
142
  mock_result = runner.invoke(mock, [self.url])
144
143
  assert mock_result.exit_code == 0
145
144
 
146
- class TestScenario():
147
-
148
- @pytest.fixture
149
- def scenario_key(self, runner: CliRunner):
150
- res = runner.invoke(scenario, ['create', '--select', 'key', '--without-headers', 'test-scenario'])
151
- assert res.exit_code == 0
152
- return ScenarioKey(res.stdout.strip())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: stoobly-agent
3
- Version: 1.5.0
3
+ Version: 1.5.2
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
@@ -1,4 +1,4 @@
1
- stoobly_agent/__init__.py,sha256=B9xjv1tEfJArVbunhXpYOoMq78yC8Szpxo5TjlWi0y4,44
1
+ stoobly_agent/__init__.py,sha256=TpjT1MsvwczXCQiCcj9IYYMVu3xK_HcFzvk6Y9h109w,44
2
2
  stoobly_agent/app/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  stoobly_agent/app/api/__init__.py,sha256=ctkB8KR-eXO0SFhj602huHiyvQ3PslFWd8fkcufgrAI,1000
4
4
  stoobly_agent/app/api/application_http_request_handler.py,sha256=ZgCfmdr5um-OFAiuRHdBxFqPIeph9WqYkVg-oVQWw-E,5543
@@ -36,7 +36,7 @@ stoobly_agent/app/cli/helpers/endpoints_apply_service.py,sha256=9GYuUlo-nNn6z7jm
36
36
  stoobly_agent/app/cli/helpers/endpoints_import_context.py,sha256=w-iRZpNh3tX8GfafEe_6QMWCBiGNNSqvJKsfAXpnrLM,2209
37
37
  stoobly_agent/app/cli/helpers/endpoints_import_service.py,sha256=FSMy3TboX6hYtYt91jk97gD8a3PpSv_RR2pl3L9-0Zs,8322
38
38
  stoobly_agent/app/cli/helpers/feature_flags.py,sha256=neIdXHhDMv8Zq1zrfoZ28NDXkamUgDiwnu-7AS3LshE,558
39
- stoobly_agent/app/cli/helpers/handle_config_update_service.py,sha256=jGBt005zkHd7TbJhICz30wPpRGqjSFwxxygjLE2CP_g,5898
39
+ stoobly_agent/app/cli/helpers/handle_config_update_service.py,sha256=0wGSvGP0t3tCFXMU872UJbyuenzwXwFw-ZAt_4oX0f8,5916
40
40
  stoobly_agent/app/cli/helpers/handle_mock_service.py,sha256=9rHhWVXniNWsAaR1jw5GgabiK5_YApq700P3F9w3gD8,701
41
41
  stoobly_agent/app/cli/helpers/handle_replay_service.py,sha256=MfCXVWoGXsuX-nt87kxLv9rScwQ_1Ec6B0m5exMAzx8,3955
42
42
  stoobly_agent/app/cli/helpers/handle_test_service.py,sha256=nlso1XXjPKoPayN13FXsE_cAhhxa8VuNCW3Md7ufqiY,7212
@@ -73,7 +73,7 @@ stoobly_agent/app/cli/scaffold/app_config.py,sha256=Gs-BynV1vY7_PpTOenn2mqc7lBIu
73
73
  stoobly_agent/app/cli/scaffold/app_create_command.py,sha256=sy4017eiir9MK0TEbTi1NyG9mhOGlenGGVnq0GsGUrQ,966
74
74
  stoobly_agent/app/cli/scaffold/command.py,sha256=aoTsdkkBzyu7TkVSMdNQQGk0Gj874jNgFcjR14y3TuM,254
75
75
  stoobly_agent/app/cli/scaffold/config.py,sha256=HZU5tkvr3dkPr4JMXZtrJlu2wxxO-134Em6jReFFcq0,688
76
- stoobly_agent/app/cli/scaffold/constants.py,sha256=zdP-hH6i2zzT_4il-o61miyNKmfQYXmfU-2nBfs2X-w,2060
76
+ stoobly_agent/app/cli/scaffold/constants.py,sha256=Vn7TLVmxbiLOb7_wFDE4mlwkcr4vs0ZEgMxhS1ANizA,2084
77
77
  stoobly_agent/app/cli/scaffold/docker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
78
  stoobly_agent/app/cli/scaffold/docker/app_builder.py,sha256=7z5pk5JKlRDHx2USxY-WurttLyyUkIVYfl34_u1x9dE,501
79
79
  stoobly_agent/app/cli/scaffold/docker/builder.py,sha256=OE7r8vEdEkW3vwpmIctePiO0S5jvao5zMJNpxc-Yayw,2851
@@ -103,8 +103,8 @@ stoobly_agent/app/cli/scaffold/service_workflow.py,sha256=sQ_Edy_wGHKMXpD0DmhnOW
103
103
  stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py,sha256=iORyPZEQurgWVxrdMgkdD3VsCz3ar2D15dmyY3RoeKw,9904
104
104
  stoobly_agent/app/cli/scaffold/templates/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
105
105
  stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context,sha256=EVoSqGJH_H7Xw1Ktan5GH2SWaOXIr916NUpptr2hkiA,158
106
- stoobly_agent/app/cli/scaffold/templates/app/.Makefile,sha256=F1OIlNw87Vw3HZqWeW908PVEnUtuIWC2gb6Bmivkr5A,8069
107
- stoobly_agent/app/cli/scaffold/templates/app/.docker-compose.base.yml,sha256=YblNDTUPndYNxupgZ3X8RP6NS4c0u_QwLklyWrpq_qs,295
106
+ stoobly_agent/app/cli/scaffold/templates/app/.Makefile,sha256=JOunQ18rT64lYqYkFb3DVXDxP5m1WCtLgy8aAlGLyLs,7999
107
+ stoobly_agent/app/cli/scaffold/templates/app/.docker-compose.base.yml,sha256=ieZmXTY0RLKowpP102uSKlfu1sbm0849T-mQz858M80,360
108
108
  stoobly_agent/app/cli/scaffold/templates/app/Makefile,sha256=TEmPG7Bf0KZOnmfsgdzza3UdwcVMmM5Lj1YdLc4cgjA,79
109
109
  stoobly_agent/app/cli/scaffold/templates/app/build/.config.yml,sha256=8Wt8ZZ5irvBYYS44xGrR_EWlZDuXH9kyWmquzsh7s8g,19
110
110
  stoobly_agent/app/cli/scaffold/templates/app/build/.docker-compose.base.yml,sha256=H43dtzQkuwWeeB-XzwQNqdKqwS04Ph_Al8AVCOjxCkQ,420
@@ -193,9 +193,9 @@ stoobly_agent/app/cli/scaffold/workflow_copy_command.py,sha256=R9hh5dWVz7vGld7pE
193
193
  stoobly_agent/app/cli/scaffold/workflow_create_command.py,sha256=bjNRph9TiOzleDiG6vYAExHWLVLAROBCPt6bPxtAmDo,3408
194
194
  stoobly_agent/app/cli/scaffold/workflow_env.py,sha256=x8V5pJmIiklD3f2q2-qq-CORf4YaXYq_r2JpR2MmSwk,416
195
195
  stoobly_agent/app/cli/scaffold/workflow_log_command.py,sha256=Bke4lMOMxuDUFuAx9nlXHbKgYMO4KAg9ASHvjz4aVWc,1372
196
- stoobly_agent/app/cli/scaffold/workflow_run_command.py,sha256=DJNyhPrvvZgOwFnPKi5Z3VdrAWfaZW_XbGfgFcnh5QM,9608
196
+ stoobly_agent/app/cli/scaffold/workflow_run_command.py,sha256=08tJIMAU3EasFAnE1hMPOFzMEYNtl4pxneAI3VcRmkg,9822
197
197
  stoobly_agent/app/cli/scaffold/workflow_validate_command.py,sha256=fhHciJXg_u32Wmh8us8LhgQj8D1SxkBADtuBBF4K0FM,4377
198
- stoobly_agent/app/cli/scaffold_cli.py,sha256=LEpcEZrr23NkLxQj_YA8tTL0YjHD-heU6LM2TxIypvg,26513
198
+ stoobly_agent/app/cli/scaffold_cli.py,sha256=xELl0zXYen7FkjLkDcb1AJXzIzbj_D519UYeYjYKjuU,26783
199
199
  stoobly_agent/app/cli/scenario_cli.py,sha256=3J1EiJOvunkfWrEkOsanw-XrKkOk78ij_GjBlE9p7CE,8229
200
200
  stoobly_agent/app/cli/snapshot_cli.py,sha256=cpCjxFYBuVwLuq_b2lIUu-5zWqupRlrp4xWgDytirSM,10047
201
201
  stoobly_agent/app/cli/trace_cli.py,sha256=K7E-vx3JUcqEDSWOdIOi_AieKNQz7dBfmRrVvKDkzFI,4605
@@ -258,12 +258,12 @@ stoobly_agent/app/models/factories/resource/local_db/helpers/search.py,sha256=A7
258
258
  stoobly_agent/app/models/factories/resource/local_db/helpers/snapshot.py,sha256=8GNlt33L4zV8yIkkhmb2iLSMYegSiP91HrPyJkE9daE,733
259
259
  stoobly_agent/app/models/factories/resource/local_db/helpers/snapshot_service.py,sha256=fbqh5J5sMj0n_YFQFei2xhXLrSGU9vNmSX_uifA3U3M,1671
260
260
  stoobly_agent/app/models/factories/resource/local_db/helpers/snapshot_types.py,sha256=1pX0jrg13632g_yv_sF1ABzPR7LY7r2HC9g1a5zR1qI,379
261
- stoobly_agent/app/models/factories/resource/local_db/helpers/tiebreak_scenario_request.py,sha256=8MKvVPfkjcJPrgle_eAE_FvcdmmGIzwrDwxi4gZmzG4,1227
261
+ stoobly_agent/app/models/factories/resource/local_db/helpers/tiebreak_scenario_request.py,sha256=BajElzsTE115hWBT6v67eyNFnrHGz-sU6WBNCzWp7Fg,1237
262
262
  stoobly_agent/app/models/factories/resource/local_db/local_db_adapter.py,sha256=uQ5wQ_n0iM8sjY5mkdqED7w4VUKFKjHQGjez1B6ruyg,384
263
263
  stoobly_agent/app/models/factories/resource/local_db/orm_request_builder.py,sha256=4UjFY26ln2Drlg71R4AKY5vUZYbQ28HYQolJv3SBDqE,1581
264
264
  stoobly_agent/app/models/factories/resource/local_db/query_param_adapter.py,sha256=GutK2RLOPvlCWDsj1PBwJFRNT7EwweQzjObvrA2cIaM,4067
265
265
  stoobly_agent/app/models/factories/resource/local_db/replayed_response_adapter.py,sha256=vUN9ARmRVrxKHpyzWB5w-XX-quOBMFpWOKkyBxTn6mM,3634
266
- stoobly_agent/app/models/factories/resource/local_db/request_adapter.py,sha256=v3BvyHZfIZaWYTqoCusM12VdsbMlcU9VSjEx9JNlXk0,12505
266
+ stoobly_agent/app/models/factories/resource/local_db/request_adapter.py,sha256=wtNj8H3pylwToc4Q0TDfl6v6rgN6ae48GcF8WZBfd40,12992
267
267
  stoobly_agent/app/models/factories/resource/local_db/response_adapter.py,sha256=g2plyjBm5NUu1W6SVZavM2PgUYW_DuynFt_MREFhPVI,3228
268
268
  stoobly_agent/app/models/factories/resource/local_db/response_header_adapter.py,sha256=lSehvihNQgX6moFY0XLDyLkTeEweCRAwQ16QSdPsvEg,3070
269
269
  stoobly_agent/app/models/factories/resource/local_db/scenario_adapter.py,sha256=K6ITkYTUeEPNGL4_skL80K5G1kgqLtlM3IwpwXsleMw,3883
@@ -292,7 +292,7 @@ stoobly_agent/app/models/schemas/request.py,sha256=WVo-v0Di-WX5sYk5fQ5piS3uo4ROz
292
292
  stoobly_agent/app/models/types/__init__.py,sha256=TdT8MVvcJ1i4VJl-JN60FJBEQeYsKClyjuOFzIsGdgI,161
293
293
  stoobly_agent/app/models/types/endpoint.py,sha256=7e60mWQs-pNFO7KmhG9wreiF9pBYB-tct-kqQi5MIVo,588
294
294
  stoobly_agent/app/models/types/replayed_response.py,sha256=VbeX2yAPWACQBc7ePQ9PfU5Ot_Vq5aKFIGrPTvMQ8Xw,403
295
- stoobly_agent/app/models/types/request.py,sha256=zA5VJ316yoMTfSoHu9ab1wbOMzS1h6G2DAhjHb80mWI,705
295
+ stoobly_agent/app/models/types/request.py,sha256=TLfgNSo7atZvCjdI_wdMRrH1qdxeO98JRNPaPjMEZHo,703
296
296
  stoobly_agent/app/models/types/request_components.py,sha256=6qEiadFnJWaTYfnxLblIOyRytLXx5FQ9yUzkWnHr0IM,471
297
297
  stoobly_agent/app/models/types/response.py,sha256=OQNvHnyLvRSJF3E4A1GaROlZekKvflzsuTZH6hDx1tY,107
298
298
  stoobly_agent/app/models/types/scenario.py,sha256=865pzF2vlhpslWf_77KTcb3XmF0lO8_LJy7qzJJNWmQ,184
@@ -300,10 +300,10 @@ stoobly_agent/app/proxy/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG
300
300
  stoobly_agent/app/proxy/constants/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
301
301
  stoobly_agent/app/proxy/constants/custom_response_codes.py,sha256=1CaApt_6W7GrxvN8_Ozbf_SEodVEQaNZRR2sMYpI0U8,40
302
302
  stoobly_agent/app/proxy/context.py,sha256=m6iu6QSZsim8meZS8H8DsZeXyjfoC-MRMHhQD1MFKM0,532
303
- stoobly_agent/app/proxy/handle_mock_service.py,sha256=TzJXeupNT74uEUfqMYPTj8l6UJgp6-Hj7qewkPqMzoQ,8422
303
+ stoobly_agent/app/proxy/handle_mock_service.py,sha256=2EHaQOcXhbeeKly-robt4tqBY3l5lkuJ4_ZkwW9r2Nw,8859
304
304
  stoobly_agent/app/proxy/handle_record_service.py,sha256=pd_3W7YxS52KYcHqzHGDIDMXQF0oLnfmnOFFQYFNWNI,3795
305
305
  stoobly_agent/app/proxy/handle_replay_service.py,sha256=kBUYd8kj8UPrsYHoBXmq06DPLjCnHo8K-QsKzhDIKcw,2526
306
- stoobly_agent/app/proxy/handle_test_service.py,sha256=Ot4YlLavDXBQp-jKjfTHZf8uoE9rCesXcG6PwwH4w6g,8014
306
+ stoobly_agent/app/proxy/handle_test_service.py,sha256=WkMPbM4argVtl-TQB7VdQIvB8cOwURAahxFX5Vkqwws,8405
307
307
  stoobly_agent/app/proxy/hot_reload.py,sha256=bcxdN0WM5l7_zY2XYLV2wF7xCpGdZILi4dVsjqAVgig,983
308
308
  stoobly_agent/app/proxy/intercept_handler.py,sha256=XlFwO501lsAgWvIwjnNmbsdXfBd2WuNG2O8L7eeiAjc,4180
309
309
  stoobly_agent/app/proxy/intercept_settings.py,sha256=KoDoUjrwvX491NHgW2M2CvaEUHOIFejiPUX4icAPZIw,11890
@@ -317,9 +317,9 @@ stoobly_agent/app/proxy/mitmproxy/response_body_facade.py,sha256=vIu6cuCSiZDocvn
317
317
  stoobly_agent/app/proxy/mitmproxy/response_facade.py,sha256=0wCSzUULUhDDV93QXUgzMNxiiSZwcu-kUB2C5z1Ck0Y,4311
318
318
  stoobly_agent/app/proxy/mock/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
319
319
  stoobly_agent/app/proxy/mock/context.py,sha256=vDo5_3WBL73mVFnsmQWvcxvPg5nWtRJbigSrE3zGc-o,794
320
- stoobly_agent/app/proxy/mock/custom_not_found_response_builder.py,sha256=Bwy7qsPzcIUw5FTaUGeaOnsTneuCCbGkTJU-wsmN3NE,415
321
- stoobly_agent/app/proxy/mock/eval_fixtures_service.py,sha256=cGoGRvn-Y8b6b2fGDODyEjcb2NK-zwk9EfXSVmSCDbA,3850
322
- stoobly_agent/app/proxy/mock/eval_request_service.py,sha256=uH0cwrn9q5Z3a-rtCKjypDwKLkoGMgnMDEVL3laO0qI,6967
320
+ stoobly_agent/app/proxy/mock/custom_not_found_response_builder.py,sha256=xHjA2CNhW5A4NyPkDzws6DY5YQeJqF6b3Y3k0iJKNss,611
321
+ stoobly_agent/app/proxy/mock/eval_fixtures_service.py,sha256=hc4VLnN50HBaWvFnrhQUJqdH-r3jBu9DHrpt8gbvkHY,3847
322
+ stoobly_agent/app/proxy/mock/eval_request_service.py,sha256=EKHZT_sKZhGcGFCDa3XOSzYkSJIfJJUg4164BTxKUeY,7365
323
323
  stoobly_agent/app/proxy/mock/hashed_request_decorator.py,sha256=h1ma90fdaYI9LBWpMWMqWBz-RjNwI628O4VuS_uUBX4,5061
324
324
  stoobly_agent/app/proxy/mock/ignored_components_response_builder.py,sha256=E32_E1eSdmPn2SeM_e1jWnqu4xh5w_SnmOs32Shx99E,501
325
325
  stoobly_agent/app/proxy/mock/request_hasher.py,sha256=WeOeksmW_b5Ql0Xb0fL2WMH3MVHrm-9GYWYoKYS1teg,3755
@@ -375,7 +375,7 @@ stoobly_agent/app/proxy/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5N
375
375
  stoobly_agent/app/proxy/utils/allowed_request_service.py,sha256=dk4xnHzCgxyvD2Ih6JDbp4_9Y7s2ar-3lpM93CW--XA,3632
376
376
  stoobly_agent/app/proxy/utils/publish_change_service.py,sha256=vSpFt0Ox9fceJlalHji61N6StdkEZwAvVXhHXO6fwEI,1259
377
377
  stoobly_agent/app/proxy/utils/request_handler.py,sha256=VscAXf2_F1C1yrt2gNImPkatosuGj356pARTd_WnTUE,846
378
- stoobly_agent/app/proxy/utils/response_handler.py,sha256=FP_4p0MQZGJdJdhdUrLH2qelq8yNKW4AQSr74ccxCj4,1429
378
+ stoobly_agent/app/proxy/utils/response_handler.py,sha256=t5spjycvPDOZi0YU39gpavnYLhb7C29HnRe9RmzGCtU,1749
379
379
  stoobly_agent/app/proxy/utils/rewrite.py,sha256=-Lf5H8aF63G37beoaG3ehr5LIAQfsKTWQvMCeNL34I4,2459
380
380
  stoobly_agent/app/proxy/utils/rewrite_rules_to_ignored_components_service.py,sha256=TEWakpZ7C468PW8lVJsRwqkRElpaCR5EOgNNzyGAleY,1043
381
381
  stoobly_agent/app/settings/__init__.py,sha256=Fm9Y4e0g_wQyPULavyv9IJopINTml7RYck6mJAPtEHY,6071
@@ -410,7 +410,7 @@ stoobly_agent/cli.py,sha256=nZDwOnhcNcZlr2hlznWVDnidPMe3LkVv88HOjamAqmA,10255
410
410
  stoobly_agent/config/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
411
411
  stoobly_agent/config/constants/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
412
412
  stoobly_agent/config/constants/alias_resolve_strategy.py,sha256=_R1tVqFnyGxCraVS5-dhSskaDj_X8-NthsY7i_bEt9M,119
413
- stoobly_agent/config/constants/custom_headers.py,sha256=IJZzZN77ZpbTr62EkXatm1r-I1_i4HEErO_LlQZLRU0,1279
413
+ stoobly_agent/config/constants/custom_headers.py,sha256=cZZ3nae2fpnk_uUXFNYjrPp7Cdtgv0ytRw-WdHzYMZg,1315
414
414
  stoobly_agent/config/constants/env_vars.py,sha256=HAR_ZIdXXbpWQgCDaRR5RtpVyGXCsMLr_Fh8n6S12K0,1344
415
415
  stoobly_agent/config/constants/headers.py,sha256=Hfv7R8_NPXAGaMiZPqywGZDnr0qcVUyfenPb4g465rE,169
416
416
  stoobly_agent/config/constants/intercept_policy.py,sha256=5hIgOft8PQmCRdOHb5OEvEj10tU66DIQF3GYltlWyM8,25
@@ -418,6 +418,7 @@ stoobly_agent/config/constants/lifecycle_hooks.py,sha256=aobclZmcND_mUnFKkUgpxgw
418
418
  stoobly_agent/config/constants/mitmproxy.py,sha256=AhaxRGMb7OC4DBqNvyBP0OVQSByCeA-itefzmMoIS14,116
419
419
  stoobly_agent/config/constants/mock_policy.py,sha256=oS3eKL5Ezpnm_4zvztcLvvtKppxm7eKvOOBHYKyKpeU,133
420
420
  stoobly_agent/config/constants/mode.py,sha256=J35vV1LPQRhD2ob6DaJ7mLGJrMhv-CVnC6IHWd7b_GM,160
421
+ stoobly_agent/config/constants/query_params.py,sha256=b-QQGKTqf-HePt0X4ltFksf5ETiIFPgBvh-kFrF7kUk,63
421
422
  stoobly_agent/config/constants/record_policy.py,sha256=n5h5Mw0-RcwEGH18koE3-Krg-ek2rRMDOHdqMxJd4Gg,181
422
423
  stoobly_agent/config/constants/replay_policy.py,sha256=FgSUaVb52wknIbTu27cWxvB35ZhxP0g1KNc6Twhtjmc,117
423
424
  stoobly_agent/config/constants/request_origin.py,sha256=zLdLbGb8TQOK5TvsPdD17SAW_OupbM3Y9mKnv7C3BRU,39
@@ -692,7 +693,7 @@ stoobly_agent/test/app/models/adapters/python/request/stoobly_adapter_test.py,sh
692
693
  stoobly_agent/test/app/models/adapters/python/response/mitmproxy_response_adapter_test.py,sha256=dEUhIH5kekZsJDwOmWz0eHA2dWZ7QPYmgq_Na9ce3HA,1176
693
694
  stoobly_agent/test/app/models/adapters/python/response/raw_response_adapter_test.py,sha256=oJ0l2FZEH3-JANcpX4N83GKxqgVkaGRKIS7rsY8O7mM,1199
694
695
  stoobly_agent/test/app/models/factories/resource/local_db/helpers/log_test.py,sha256=FbGtjO_K_L2lZw1i6b1LlZoUyhisYqVST7f9T0PmpFk,3501
695
- stoobly_agent/test/app/models/factories/resource/local_db/helpers/tiebreak_scenario_request_test.py,sha256=5IFGVGn3ogvRT0fzZgXzhrYlSfSlr8IR1p_XExaegMQ,3399
696
+ stoobly_agent/test/app/models/factories/resource/local_db/helpers/tiebreak_scenario_request_test.py,sha256=a1SFLyEyRRLuADvAw6ckQQKORFXvyK1lyrbkaLWx8oU,3399
696
697
  stoobly_agent/test/app/models/factories/resource/local_db/request_adapter_test.py,sha256=Pzq1cBPnP9oSWG-p0c-VoymoHxgp483QmNwmV1b78RA,8453
697
698
  stoobly_agent/test/app/models/factories/resource/local_db/response_adapter_test.py,sha256=9P95EKH5rZGOrmRkRIDlQZqtiLJHk9735og18Ffwpfw,2204
698
699
  stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION,sha256=H03AltWPfSHjh1ZxrubymxIKuEIY-kfbLLU7yetbTaw,5
@@ -714,7 +715,9 @@ stoobly_agent/test/app/test/matchers/contract_test.py,sha256=fMV8BX4JfbIOD5i7ZV7
714
715
  stoobly_agent/test/app/test/matchers/diff_test.py,sha256=fNbzAz3MptieVIrMInxGlNa4FsBh-VQ2m7-OsrNm9sw,10266
715
716
  stoobly_agent/test/app/test/matchers/fuzzy_test.py,sha256=wkySpK00dFZLlXmW7xE7Q6UPJYLiq8aCnIHt4Nm4WEk,3231
716
717
  stoobly_agent/test/cli/lifecycle_hooks_test.py,sha256=zjLI8zteszWXgQuyEYM47MtI8KgLbEFVYJTaJdyBqOc,2491
717
- stoobly_agent/test/cli/mock_test.py,sha256=zhIPJFb4AxRR6ltHd_Su_BKMwQtZLy0Jx6VnkEELfO8,5983
718
+ stoobly_agent/test/cli/mock_scenario_lifecycle_hooks.py,sha256=e57A3FWHt1ZgaWKhbl4Hs2Y4ux8tKMu9nxbqPCj6w54,254
719
+ stoobly_agent/test/cli/mock_scenario_test.py,sha256=tHT-p8Vm8APP2KOlyfZ-5z_AxZO9TS4sDgui3X2qkaE,2620
720
+ stoobly_agent/test/cli/mock_test.py,sha256=zYAFhHkR-1oafZzzL_4aPOWzM2nXWRHGt36yTAuG8Rw,5636
718
721
  stoobly_agent/test/cli/record_test.py,sha256=CG7prrooTgkW8RYaiDRYD3LuuDIR8dz8jaP9RNRrF4o,15814
719
722
  stoobly_agent/test/config/data_dir_test.py,sha256=sszUW6MKGFCS9JGhYL-E91hBzfI2jMaHRJMifNjZ00M,2659
720
723
  stoobly_agent/test/mock_data/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -734,8 +737,8 @@ stoobly_agent/test/mock_data/scaffold/docker-compose-local-service.yml,sha256=x7
734
737
  stoobly_agent/test/mock_data/scaffold/index.html,sha256=qJwuYajKZ4ihWZrJQ3BNObV5kf1VGnnm_vqlPJzdqLE,258
735
738
  stoobly_agent/test/mock_data/uspto.yaml,sha256=6U5se7C3o-86J4m9xpOk9Npias399f5CbfWzR87WKwE,7835
736
739
  stoobly_agent/test/test_helper.py,sha256=m_oAI7tmRYCNZdKfNqISWhMv3e44tjeYViQ3nTUfnos,1007
737
- stoobly_agent-1.5.0.dist-info/LICENSE,sha256=o93sj12cdoEOsTCjPaPFsw3Xq0SXs3pPcY-9reE2sEw,548
738
- stoobly_agent-1.5.0.dist-info/METADATA,sha256=7g-q1O6IbKxg8zRdiyxSRGKBND6LLDeWTR9CBcDSzII,3087
739
- stoobly_agent-1.5.0.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
740
- stoobly_agent-1.5.0.dist-info/entry_points.txt,sha256=aq5wix5oC8MDQtmyPGU0xaFrsjJg7WH28NmXh2sc3Z8,56
741
- stoobly_agent-1.5.0.dist-info/RECORD,,
740
+ stoobly_agent-1.5.2.dist-info/LICENSE,sha256=o93sj12cdoEOsTCjPaPFsw3Xq0SXs3pPcY-9reE2sEw,548
741
+ stoobly_agent-1.5.2.dist-info/METADATA,sha256=h0M6PU0p1COzIMJABfrZtaWjTshuY-Jk2gudZzsj958,3087
742
+ stoobly_agent-1.5.2.dist-info/WHEEL,sha256=XbeZDeTWKc1w7CSIyre5aMDU_-PohRwTQceYnisIYYY,88
743
+ stoobly_agent-1.5.2.dist-info/entry_points.txt,sha256=aq5wix5oC8MDQtmyPGU0xaFrsjJg7WH28NmXh2sc3Z8,56
744
+ stoobly_agent-1.5.2.dist-info/RECORD,,