stoobly-agent 1.9.12__py3-none-any.whl → 1.10.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 (98) hide show
  1. stoobly_agent/__init__.py +1 -1
  2. stoobly_agent/app/api/__init__.py +4 -20
  3. stoobly_agent/app/api/application_http_request_handler.py +5 -2
  4. stoobly_agent/app/api/configs_controller.py +3 -3
  5. stoobly_agent/app/cli/decorators/exec.py +1 -1
  6. stoobly_agent/app/cli/helpers/handle_config_update_service.py +4 -0
  7. stoobly_agent/app/cli/intercept_cli.py +40 -7
  8. stoobly_agent/app/cli/scaffold/app_command.py +4 -0
  9. stoobly_agent/app/cli/scaffold/app_config.py +21 -3
  10. stoobly_agent/app/cli/scaffold/app_create_command.py +109 -2
  11. stoobly_agent/app/cli/scaffold/constants.py +14 -3
  12. stoobly_agent/app/cli/scaffold/docker/constants.py +4 -6
  13. stoobly_agent/app/cli/scaffold/docker/service/build_decorator.py +2 -2
  14. stoobly_agent/app/cli/scaffold/docker/service/builder.py +36 -10
  15. stoobly_agent/app/cli/scaffold/docker/workflow/builder.py +0 -27
  16. stoobly_agent/app/cli/scaffold/docker/workflow/command_decorator.py +25 -0
  17. stoobly_agent/app/cli/scaffold/docker/workflow/decorators_factory.py +7 -2
  18. stoobly_agent/app/cli/scaffold/docker/workflow/detached_decorator.py +42 -0
  19. stoobly_agent/app/cli/scaffold/docker/workflow/local_decorator.py +26 -0
  20. stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py +9 -10
  21. stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py +5 -8
  22. stoobly_agent/app/cli/scaffold/service_config.py +133 -34
  23. stoobly_agent/app/cli/scaffold/service_create_command.py +11 -2
  24. stoobly_agent/app/cli/scaffold/service_dependency.py +51 -0
  25. stoobly_agent/app/cli/scaffold/service_docker_compose.py +3 -3
  26. stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py +10 -7
  27. stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context +1 -1
  28. stoobly_agent/app/cli/scaffold/templates/app/build/.docker-compose.base.yml +2 -2
  29. stoobly_agent/app/cli/scaffold/templates/app/build/mock/bin/configure +1 -1
  30. stoobly_agent/app/cli/scaffold/templates/app/build/mock/docker-compose.yml +16 -6
  31. stoobly_agent/app/cli/scaffold/templates/app/build/record/bin/configure +26 -1
  32. stoobly_agent/app/cli/scaffold/templates/app/build/record/docker-compose.yml +16 -6
  33. stoobly_agent/app/cli/scaffold/templates/app/build/test/bin/configure +1 -1
  34. stoobly_agent/app/cli/scaffold/templates/app/build/test/docker-compose.yml +16 -6
  35. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/.docker-compose.base.yml +2 -2
  36. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/bin/configure +1 -1
  37. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/mock/docker-compose.yml +16 -10
  38. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/bin/configure +1 -1
  39. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/record/docker-compose.yml +16 -10
  40. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/bin/configure +1 -1
  41. stoobly_agent/app/cli/scaffold/templates/app/entrypoint/test/docker-compose.yml +16 -10
  42. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/.docker-compose.base.yml +2 -1
  43. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/mock/.docker-compose.mock.yml +6 -3
  44. stoobly_agent/app/cli/scaffold/templates/app/stoobly-ui/record/.docker-compose.record.yml +6 -4
  45. stoobly_agent/app/cli/scaffold/templates/build/workflows/record/.configure +21 -1
  46. stoobly_agent/app/cli/scaffold/templates/constants.py +4 -0
  47. stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.Dockerfile.cypress +22 -0
  48. stoobly_agent/app/cli/scaffold/templates/plugins/cypress/test/.docker-compose.test.yml +19 -0
  49. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.Dockerfile.playwright +33 -0
  50. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.docker-compose.test.yml +18 -0
  51. stoobly_agent/app/cli/scaffold/templates/plugins/playwright/test/.entrypoint.sh +11 -0
  52. stoobly_agent/app/cli/scaffold/templates/workflow/mock/bin/configure +2 -10
  53. stoobly_agent/app/cli/scaffold/templates/workflow/mock/docker-compose.yml +17 -0
  54. stoobly_agent/app/cli/scaffold/templates/workflow/record/bin/configure +19 -45
  55. stoobly_agent/app/cli/scaffold/templates/workflow/record/docker-compose.yml +17 -0
  56. stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/configure +2 -10
  57. stoobly_agent/app/cli/scaffold/templates/workflow/test/docker-compose.yml +17 -0
  58. stoobly_agent/app/cli/scaffold/workflow_create_command.py +0 -1
  59. stoobly_agent/app/cli/scaffold/workflow_run_command.py +1 -1
  60. stoobly_agent/app/cli/scaffold_cli.py +85 -96
  61. stoobly_agent/app/proxy/handle_record_service.py +12 -3
  62. stoobly_agent/app/proxy/handle_replay_service.py +14 -2
  63. stoobly_agent/app/proxy/intercept_settings.py +12 -8
  64. stoobly_agent/app/proxy/record/upload_request_service.py +5 -8
  65. stoobly_agent/app/proxy/replay/replay_request_service.py +3 -0
  66. stoobly_agent/app/proxy/run.py +3 -28
  67. stoobly_agent/app/proxy/utils/allowed_request_service.py +3 -2
  68. stoobly_agent/app/proxy/utils/minimize_headers.py +47 -0
  69. stoobly_agent/app/proxy/utils/publish_change_service.py +22 -24
  70. stoobly_agent/app/proxy/utils/strategy.py +16 -0
  71. stoobly_agent/app/settings/__init__.py +15 -6
  72. stoobly_agent/app/settings/data_rules.py +25 -1
  73. stoobly_agent/app/settings/intercept_settings.py +5 -2
  74. stoobly_agent/app/settings/types/__init__.py +0 -1
  75. stoobly_agent/app/settings/ui_settings.py +5 -5
  76. stoobly_agent/cli.py +41 -16
  77. stoobly_agent/config/constants/custom_headers.py +1 -0
  78. stoobly_agent/config/constants/env_vars.py +4 -3
  79. stoobly_agent/config/constants/record_strategy.py +6 -0
  80. stoobly_agent/config/data_dir.py +1 -0
  81. stoobly_agent/config/settings.yml.sample +2 -3
  82. stoobly_agent/lib/logger.py +15 -5
  83. stoobly_agent/public/index.html +1 -1
  84. stoobly_agent/public/main-es2015.5a9aa16433404c3f423a.js +1 -0
  85. stoobly_agent/public/main-es5.5a9aa16433404c3f423a.js +1 -0
  86. stoobly_agent/test/app/cli/intercept/intercept_configure_test.py +231 -1
  87. stoobly_agent/test/app/cli/scaffold/cli_invoker.py +3 -2
  88. stoobly_agent/test/app/cli/scaffold/cli_test.py +3 -3
  89. stoobly_agent/test/app/cli/scaffold/e2e_test.py +11 -11
  90. stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION +1 -1
  91. stoobly_agent/test/app/proxy/utils/minimize_headers_test.py +342 -0
  92. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/METADATA +2 -1
  93. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/RECORD +96 -80
  94. stoobly_agent/public/main-es2015.089b46f303768fbe864f.js +0 -1
  95. stoobly_agent/public/main-es5.089b46f303768fbe864f.js +0 -1
  96. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/LICENSE +0 -0
  97. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/WHEEL +0 -0
  98. {stoobly_agent-1.9.12.dist-info → stoobly_agent-1.10.1.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,47 @@
1
+ import pdb
2
+
3
+ from mitmproxy.http import Headers
4
+ from mitmproxy.http import HTTPFlow as MitmproxyHTTPFlow
5
+
6
+ from typing import Final
7
+
8
+
9
+ REQUEST_HEADERS_ALLOWLIST: Final[dict[str]] = {
10
+ "Accept",
11
+ "Accept-Encoding",
12
+ "Accept-Language",
13
+ "Content-Length",
14
+ "Content-Type",
15
+ "Host",
16
+ "Origin",
17
+ "Referer",
18
+ "User-Agent",
19
+ }
20
+
21
+ RESPONSE_HEADERS_ALLOWLIST: Final[dict[str]] = {
22
+ "Content-Length",
23
+ "Content-Type",
24
+ "Date",
25
+ "Transfer-Encoding",
26
+ "Server", # Sometimes required for HTTP/1.0, but not strictly mandatory
27
+ }
28
+
29
+ def minimize_headers(flow: MitmproxyHTTPFlow):
30
+ minimize_request_headers(flow)
31
+ minimize_response_headers(flow)
32
+
33
+ def minimize_request_headers(flow: MitmproxyHTTPFlow) -> None:
34
+ remove_headers(flow.request.headers, REQUEST_HEADERS_ALLOWLIST)
35
+
36
+ def minimize_response_headers(flow: MitmproxyHTTPFlow) -> None:
37
+ remove_headers(flow.response.headers, RESPONSE_HEADERS_ALLOWLIST)
38
+
39
+ def remove_headers(headers: Headers, allowlist: dict[str]):
40
+ keys_to_remove = []
41
+
42
+ for key in headers:
43
+ if key.lower() not in {allowed_header.lower() for allowed_header in allowlist}:
44
+ keys_to_remove.append(key)
45
+
46
+ for key in keys_to_remove:
47
+ headers.pop(key)
@@ -1,40 +1,35 @@
1
1
  import pdb
2
+ import requests
2
3
  import threading
3
4
 
4
- from typing import TypedDict
5
-
6
- from stoobly_agent.config.constants.statuses import REQUESTS_MODIFIED
5
+ from stoobly_agent.app.settings import Settings
6
+ from stoobly_agent.config.constants.statuses import REQUESTS_MODIFIED, SETTINGS_MODIFIED
7
7
  from stoobly_agent.lib.api.agent_api import AgentApi
8
8
  from stoobly_agent.lib.cache import Cache
9
9
  from stoobly_agent.lib.logger import Logger
10
10
 
11
- class Options(TypedDict):
12
- sync: bool
13
-
14
- # Announce that a new request has been created
15
- def publish_change(status: str, value: any, **options: Options):
16
- if options.get('sync'):
17
- return __publish_change_sync(status, value)
11
+ LOG_ID = 'PublishChange'
18
12
 
19
- from stoobly_agent.app.settings import Settings
20
- settings = Settings.instance()
13
+ def publish_settings_modified(value):
14
+ return __publish_change_sync(SETTINGS_MODIFIED, value)
15
+
16
+ def publish_requests_modified(value):
17
+ settings: Settings = Settings.instance()
21
18
 
22
- # If ui is not active, return
23
- if not settings.ui.active:
24
- return False
19
+ # If not headless...
20
+ if settings.ui.active:
21
+ return __publish_change_sync(REQUESTS_MODIFIED, value)
25
22
 
26
23
  ui_url = settings.ui.url
27
-
28
24
  if not ui_url:
29
- Logger.instance().warn('Settings.ui.url not configured')
30
25
  return False
31
- else:
32
- thread = threading.Thread(target=__put_status, args=(ui_url, status, value))
33
- thread.start()
34
- return True
35
26
 
36
- def publish_requests_modified(value, **options: Options):
37
- return publish_change(REQUESTS_MODIFIED, value, **options)
27
+ return __publish_change_async(REQUESTS_MODIFIED, value, ui_url)
28
+
29
+ def __publish_change_async(status, value, ui_url: str):
30
+ thread = threading.Thread(target=__put_status, args=(ui_url, status, value))
31
+ thread.start()
32
+ return True
38
33
 
39
34
  def __publish_change_sync(status: str, value):
40
35
  cache = Cache.instance()
@@ -43,4 +38,7 @@ def __publish_change_sync(status: str, value):
43
38
 
44
39
  def __put_status(ui_url, status, value):
45
40
  api: AgentApi = AgentApi(ui_url)
46
- api.update_status(status, value)
41
+ try:
42
+ api.update_status(status, value)
43
+ except requests.exceptions.ConnectionError:
44
+ Logger.instance(LOG_ID).error(f"could not connect to {ui_url}")
@@ -0,0 +1,16 @@
1
+ import pdb
2
+
3
+ from stoobly_agent.app.proxy.intercept_settings import InterceptSettings
4
+ from stoobly_agent.app.settings.constants import intercept_mode
5
+ from stoobly_agent.config.constants import request_origin
6
+
7
+
8
+ def get_active_mode_strategy(intercept_settings: InterceptSettings) -> str:
9
+ strategy = ""
10
+
11
+ if intercept_settings.mode == intercept_mode.RECORD:
12
+ strategy = intercept_settings.record_strategy
13
+ elif intercept_settings.mode == intercept_mode.TEST:
14
+ strategy = intercept_settings.test_strategy
15
+
16
+ return strategy
@@ -3,12 +3,12 @@ import pdb
3
3
  import time
4
4
  import yaml
5
5
 
6
- from shutil import copyfile
6
+ from filelock import FileLock
7
7
  from watchdog.observers import Observer
8
8
  from watchdog.events import PatternMatchingEventHandler
9
9
  from yamale import *
10
10
 
11
- from stoobly_agent.config.constants import env_vars, statuses
11
+ from stoobly_agent.config.constants import env_vars
12
12
  from stoobly_agent.config.data_dir import DataDir
13
13
  from stoobly_agent.config.source_dir import SourceDir
14
14
  from stoobly_agent.lib.logger import Logger
@@ -19,6 +19,7 @@ from .remote_settings import RemoteSettings
19
19
  from .ui_settings import UISettings
20
20
 
21
21
  LOG_ID = 'Settings'
22
+ SETTINGS_YML = 'settings.yml'
22
23
 
23
24
  class Settings:
24
25
  _instances = None
@@ -102,7 +103,7 @@ class Settings:
102
103
  if self.__watching:
103
104
  return False
104
105
 
105
- patterns = ['settings.yml']
106
+ patterns = [SETTINGS_YML]
106
107
  ignore_patterns = None
107
108
  ignore_directories = False
108
109
  case_sensitive = True
@@ -177,7 +178,14 @@ class Settings:
177
178
  self.__load_settings()
178
179
 
179
180
  def write(self, contents):
180
- if contents:
181
+ if not contents:
182
+ return
183
+
184
+ lock_file = f".{SETTINGS_YML}.lock"
185
+ lock_file_path = os.path.join(os.path.dirname(self.__settings_file_path), lock_file)
186
+ lock = FileLock(lock_file_path) # lock file alongside the target
187
+
188
+ with lock:
181
189
  with open(self.__settings_file_path, 'w') as fp:
182
190
  yaml.dump(contents, fp, allow_unicode=True)
183
191
 
@@ -209,13 +217,14 @@ class Settings:
209
217
 
210
218
  def __reload_settings(self, event):
211
219
  if not self.__load_lock:
212
- from stoobly_agent.app.proxy.utils.publish_change_service import publish_change
220
+ from stoobly_agent.app.proxy.utils.publish_change_service import publish_settings_modified
213
221
 
214
222
  self.__load_lock = True
215
223
 
216
224
  Logger.instance(LOG_ID).debug('Reloading settings')
217
225
  self.__load_settings()
218
226
 
219
- publish_change(statuses.SETTINGS_MODIFIED, self.__settings, sync=True)
227
+ if self.__ui_settings.active:
228
+ publish_settings_modified(self.__settings)
220
229
 
221
230
  self.__load_lock = False
@@ -1,4 +1,4 @@
1
- from stoobly_agent.config.constants import mock_policy, record_order, record_policy, replay_policy, test_strategy
1
+ from stoobly_agent.config.constants import mock_policy, record_order, record_policy, record_strategy, replay_policy, test_strategy
2
2
 
3
3
  from .types.proxy_settings import DataRules as IDataRules
4
4
 
@@ -10,6 +10,7 @@ class DataRules:
10
10
  self.__mock_policy = self.__data_rules.get('mock_policy') or mock_policy.FOUND
11
11
  self.__record_order = self.__data_rules.get('record_order') or record_order.APPEND
12
12
  self.__record_policy = self.__data_rules.get('record_policy') or record_policy.ALL
13
+ self.__record_strategy = self.__data_rules.get('record_strategy') or record_strategy.FULL
13
14
  self.__replay_policy = self.__data_rules.get('replay_policy') or replay_policy.ALL
14
15
  self.__scenario_key = self.__data_rules.get('scenario_key')
15
16
  self.__test_policy = self.__data_rules.get('test_policy') or mock_policy.FOUND
@@ -46,6 +47,19 @@ class DataRules:
46
47
  self.__record_order = v
47
48
  self.__data_rules['record_order'] = v
48
49
 
50
+ @property
51
+ def record_strategy(self):
52
+ return self.__record_strategy
53
+
54
+ @record_strategy.setter
55
+ def record_strategy(self, v):
56
+ valid_strategies = [record_strategy.FULL, record_strategy.MINIMAL]
57
+ if v not in valid_strategies:
58
+ raise TypeError(f"record_strategy has to be one of {valid_strategies}, got {v}")
59
+
60
+ self.__record_strategy = v
61
+ self.__data_rules['record_strategy'] = v
62
+
49
63
  @property
50
64
  def replay_policy(self):
51
65
  return self.__replay_policy
@@ -79,11 +93,21 @@ class DataRules:
79
93
  def test_strategy(self):
80
94
  return self.__test_strategy
81
95
 
96
+ @test_strategy.setter
97
+ def test_strategy(self, v):
98
+ valid_strategies = [test_strategy.CONTRACT, test_strategy.CUSTOM, test_strategy.DIFF, test_strategy.FUZZY]
99
+ if v not in valid_strategies:
100
+ raise TypeError(f"test_strategy has to be one of {valid_strategies}, got {v}")
101
+
102
+ self.__test_strategy = v
103
+ self.__data_rules['test_strategy'] = v
104
+
82
105
  def to_dict(self) -> IDataRules:
83
106
  return {
84
107
  'mock_policy': self.__mock_policy,
85
108
  'record_order': self.__record_order,
86
109
  'record_policy': self.__record_policy,
110
+ 'record_strategy': self.__record_strategy,
87
111
  'replay_policy': self.__replay_policy,
88
112
  'scenario_key': self.__scenario_key,
89
113
  'test_policy': self.__test_policy,
@@ -19,6 +19,9 @@ class InterceptSettings:
19
19
 
20
20
  @property
21
21
  def active(self):
22
+ if os.environ.get(env_vars.AGENT_INTERCEPT_ACTIVE):
23
+ return os.environ[env_vars.AGENT_INTERCEPT_ACTIVE]
24
+
22
25
  return self.__active
23
26
 
24
27
  @active.setter
@@ -35,8 +38,8 @@ class InterceptSettings:
35
38
  if self.__mode != self.mode_before_change:
36
39
  return self.__mode
37
40
 
38
- if os.environ.get(env_vars.AGENT_ACTIVE_MODE):
39
- return os.environ[env_vars.AGENT_ACTIVE_MODE]
41
+ if os.environ.get(env_vars.AGENT_INTERCEPT_MODE):
42
+ return os.environ[env_vars.AGENT_INTERCEPT_MODE]
40
43
 
41
44
  return self.__mode
42
45
 
@@ -84,7 +84,6 @@ class ISettings(TypedDict):
84
84
  api_url: str
85
85
  api_key: str
86
86
  mode: ISettingsMode
87
- proxy_config_path: str
88
87
 
89
88
  Component = {
90
89
  'Header': 'Header',
@@ -21,8 +21,8 @@ class UISettings:
21
21
  if self.__active != self.active_before_change:
22
22
  return self.__active
23
23
 
24
- if os.environ.get(env_vars.AGENT_IS_HEADLESS):
25
- return True
24
+ if os.environ.get(env_vars.AGENT_HEADLESS):
25
+ return False
26
26
 
27
27
  return self.__active or False
28
28
 
@@ -39,8 +39,8 @@ class UISettings:
39
39
  if self.__url != self.url_before_change:
40
40
  return self.__url
41
41
 
42
- if os.environ.get(env_vars.AGENT_URL):
43
- return os.environ[env_vars.AGENT_URL]
42
+ if os.environ.get(env_vars.AGENT_UI_URL):
43
+ return os.environ[env_vars.AGENT_UI_URL]
44
44
 
45
45
  return self.__url or ''
46
46
 
@@ -52,4 +52,4 @@ class UISettings:
52
52
  return {
53
53
  'active': self.__active,
54
54
  'url': self.__url,
55
- }
55
+ }
stoobly_agent/cli.py CHANGED
@@ -13,9 +13,10 @@ from stoobly_agent.app.proxy.constants import custom_response_codes
13
13
  from stoobly_agent.app.proxy.replay.replay_request_service import replay as replay_request
14
14
  from stoobly_agent.config.constants import env_vars, mode
15
15
  from stoobly_agent.config.data_dir import DataDir
16
+ from stoobly_agent.lib.logger import Logger
16
17
  from stoobly_agent.lib.utils.conditional_decorator import ConditionalDecorator
17
18
 
18
- from .app.api import initialize as initialize_api, run as run_api
19
+ from .app.api import run as run_api
19
20
  from .app.cli import ca_cert, config, endpoint, feature, intercept, MainGroup, request, scenario, scaffold, snapshot, trace
20
21
  from .app.cli.helpers.feature_flags import local, remote
21
22
  from .app.settings import Settings
@@ -99,6 +100,7 @@ def init(**kwargs):
99
100
  ''')
100
101
  @click.option('--headless', is_flag=True, default=False, help='Disable starting UI.')
101
102
  @click.option('--intercept', is_flag=True, default=False, help='Enable intercept on run.')
103
+ @click.option('--intercept-mode', help='Set intercept mode.')
102
104
  @click.option('--log-level', default=logger.INFO, type=click.Choice([logger.DEBUG, logger.INFO, logger.WARNING, logger.ERROR]), help='''
103
105
  Log levels can be "debug", "info", "warning", or "error"
104
106
  ''')
@@ -124,7 +126,19 @@ def init(**kwargs):
124
126
  def run(**kwargs):
125
127
  from .app.proxy.run import run as run_proxy
126
128
 
127
- os.environ[env_vars.AGENT_PROXY_URL] = f"http://{kwargs['proxy_host']}:{kwargs['proxy_port']}"
129
+ # Observe config for changes
130
+ settings: Settings = Settings.instance()
131
+ settings.watch()
132
+
133
+ if kwargs.get('headless'):
134
+ os.environ[env_vars.AGENT_HEADLESS] = '1'
135
+
136
+ if kwargs.get('intercept'):
137
+ os.environ[env_vars.AGENT_INTERCEPT_ACTIVE] = '1'
138
+
139
+ if kwargs.get('intercept_mode'):
140
+ os.environ[env_vars.AGENT_INTERCEPT_MODE] = kwargs['intercept_mode']
141
+ settings.proxy.intercept.mode = kwargs['intercept_mode']
128
142
 
129
143
  if kwargs.get('lifecycle_hooks_path'):
130
144
  os.environ[env_vars.AGENT_LIFECYCLE_HOOKS_PATH] = kwargs['lifecycle_hooks_path']
@@ -135,21 +149,32 @@ def run(**kwargs):
135
149
  if kwargs.get('response_fixtures_path'):
136
150
  os.environ[env_vars.AGENT_RESPONSE_FIXTURES_PATH] = kwargs['response_fixtures_path']
137
151
 
138
- # Observe config for changes
139
- Settings.instance().watch()
140
-
141
152
  if not os.getenv(env_vars.LOG_LEVEL):
142
- os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
143
-
144
- if 'api_url' in kwargs and kwargs['api_url']:
145
- os.environ[env_vars.API_URL] = kwargs['api_url']
146
-
147
- url = initialize_api(**kwargs)
148
- if 'headless' in kwargs and not kwargs['headless']:
149
- run_api(url)
150
-
151
- if 'proxyless' in kwargs and not kwargs['proxyless']:
152
- run_proxy(**kwargs)
153
+ os.environ[env_vars.LOG_LEVEL] = kwargs['log_level']
154
+
155
+ if kwargs.get('api_url'):
156
+ os.environ[env_vars.API_URL] = kwargs['api_url']
157
+
158
+ if not kwargs.get('headless'):
159
+ ui_url = f"http://{kwargs['ui_host']}:{kwargs['ui_port']}"
160
+ os.environ[env_vars.AGENT_UI_URL] = ui_url
161
+ settings.ui.active = True
162
+ settings.ui.url = ui_url
163
+
164
+ if not kwargs.get('proxyless'):
165
+ proxy_url = f"http://{kwargs['proxy_host']}:{kwargs['proxy_port']}"
166
+ os.environ[env_vars.AGENT_PROXY_URL] = proxy_url
167
+ settings.proxy.url = proxy_url
168
+
169
+ if not kwargs.get('headless'):
170
+ settings.commit()
171
+ run_api(**kwargs)
172
+
173
+ if not kwargs.get('proxyless'):
174
+ log_id = 'Proxy'
175
+ Logger.instance(log_id).info(f"starting with mode {kwargs['proxy_mode']} and listening at {kwargs['proxy_host']}:{kwargs['proxy_port']}")
176
+ Logger.instance(log_id).info(f"{'' if settings.proxy.intercept.active else 'not yet '}configured to {settings.proxy.intercept.mode}")
177
+ run_proxy(**kwargs)
153
178
 
154
179
  @main.command(
155
180
  help="Mock request"
@@ -13,6 +13,7 @@ PROJECT_KEY = 'X-Stoobly-Project-Key'
13
13
  PROXY_MODE = 'X-Stoobly-Proxy-Mode'
14
14
  PUBLIC_DIRECTORY_PATH = 'X-Stoobly-Public-Directory-Path'
15
15
  RECORD_POLICY = 'X-Stoobly-Record-Policy'
16
+ RECORD_STRATEGY = 'X-Stoobly-Record-Strategy'
16
17
  REPORT_KEY = 'X-Stoobly-Report-Key'
17
18
  REQUEST_ORIGIN = 'X-Stoobly-Request-Origin'
18
19
  RESPONSE_FIXTURES_PATH = 'X-Stoobly-Response-Fixtures-Path'
@@ -1,9 +1,10 @@
1
- AGENT_ACTIVE_MODE = 'STOOBLY_AGENT_ACTIVE_MODE'
2
1
  AGENT_CONFIG_PATH = 'STOOBLY_AGENT_CONFIG_PATH'
3
2
  AGENT_ENABLED = 'STOOBLY_AGENT_ENABLED'
4
3
  AGENT_SELF_INTERCEPT_ENABLED = 'STOOBLY_AGENT_SELF_INTERCEPT_ENABLED'
4
+ AGENT_HEADLESS = 'STOOBLY_AGENT_HEADLESS'
5
5
  AGENT_INCLUDE_PATTERNS = 'STOOBLY_AGENT_INCLUDE_PATTERNS'
6
- AGENT_IS_HEADLESS = 'STOOBLY_AGENT_IS_HEADLESS'
6
+ AGENT_INTERCEPT_ACTIVE = 'STOOBLY_AGENT_INTERCEPT_ACTIVE'
7
+ AGENT_INTERCEPT_MODE = 'STOOBLY_AGENT_INTERCEPT_MODE'
7
8
  AGENT_EXCLUDE_PATTERNS = 'STOOBLY_AGENT_EXCLUDE_PATTERNS'
8
9
  AGENT_LIFECYCLE_HOOKS_PATH = 'STOOBLY_AGENT_LIFECYCLE_HOOKS_PATH'
9
10
  AGENT_POLICY = 'STOOBLY_AGENT_POLICY'
@@ -18,7 +19,7 @@ AGENT_RESPONSE_FIXTURES_PATH = 'STOOBLY_AGENT_RESPONSE_FIXTURES_PATH'
18
19
  AGENT_SERVICE_URL = 'STOOBLY_AGENT_SERVICE_URL'
19
20
  AGENT_SCENARIO_KEY = 'STOOBLY_AGENT_SCENARIO_KEY'
20
21
  AGENT_SIMULATE_LATENCY = 'STOOBLY_AGENT_SIMULATE_LATENCY'
21
- AGENT_URL = 'STOOBLY_AGENT_URL'
22
+ AGENT_UI_URL = 'STOOBLY_AGENT_UI_URL'
22
23
  API_URL = 'STOOBLY_API_URL'
23
24
  API_KEY = 'STOOBLY_API_KEY'
24
25
  ENV = 'STOOBLY_AGENT_ENV'
@@ -0,0 +1,6 @@
1
+ from typing import Literal
2
+
3
+ FULL = 'full'
4
+ MINIMAL = 'minimal'
5
+
6
+ RecordStrategy = Literal[FULL, MINIMAL]
@@ -202,6 +202,7 @@ class DataDir:
202
202
  with open(os.path.join(self.__data_dir_path, '.gitignore'), 'w') as fp:
203
203
  fp.write(
204
204
  "\n".join([
205
+ '.settings.yml.lock',
205
206
  'ca_certs',
206
207
  'certs',
207
208
  'db',
@@ -12,10 +12,9 @@ proxy:
12
12
  project_key: eyJpIjowLCJvIjowfQ==
13
13
  firewall: {}
14
14
  rewrite: {}
15
- proxy_config_path: config/settings.yml
16
15
  remote:
17
16
  api_key: ''
18
- api_url: 'http://localhost:3000'
17
+ api_url: ''
19
18
  ui:
20
19
  active: false
21
- url: http://localhost:4200
20
+ url: ''
@@ -27,6 +27,7 @@ LogLevel = Literal[DEBUG, ERROR, INFO, WARNING]
27
27
 
28
28
  class Logger:
29
29
  _instance = None
30
+ _instances = {}
30
31
 
31
32
  def __init__(self):
32
33
  raise RuntimeError('Call instance() instead')
@@ -34,14 +35,23 @@ class Logger:
34
35
  @classmethod
35
36
  def instance(cls, name = None):
36
37
  if cls._instance is None:
38
+ logging.getLogger("mitmproxy").setLevel(logging.ERROR)
37
39
  cls._instance = cls.__new__(cls)
38
-
39
- cls._instance.load()
40
+ cls._instance.load()
40
41
 
41
42
  if not name:
42
- return logging
43
- else:
44
- return logging.getLogger(name)
43
+ name = 'root'
44
+
45
+ logger = logging.getLogger(name)
46
+
47
+ if name not in cls._instances:
48
+ handler = logging.StreamHandler()
49
+ handler.setFormatter(logging.Formatter(f"[%(levelname)s] {name} %(message)s"))
50
+ logger.addHandler(handler)
51
+ logger.propagate = False
52
+ cls._instances[name] = logger
53
+
54
+ return logger
45
55
 
46
56
  @classmethod
47
57
  def reload(cls):
@@ -113,6 +113,6 @@
113
113
  </div>
114
114
 
115
115
  <root></root>
116
- <script src="runtime-es2015.f8c814b38b27708e91c1.js" type="module"></script><script src="runtime-es5.f8c814b38b27708e91c1.js" nomodule="" defer=""></script><script src="polyfills-es5.7530172ddcec11a10eb3.js" nomodule="" defer=""></script><script src="polyfills-es2015.8ce2adc69f283f6c4c5e.js" type="module"></script><script src="main-es2015.089b46f303768fbe864f.js" type="module"></script><script src="main-es5.089b46f303768fbe864f.js" nomodule="" defer=""></script></body>
116
+ <script src="runtime-es2015.f8c814b38b27708e91c1.js" type="module"></script><script src="runtime-es5.f8c814b38b27708e91c1.js" nomodule="" defer=""></script><script src="polyfills-es5.7530172ddcec11a10eb3.js" nomodule="" defer=""></script><script src="polyfills-es2015.8ce2adc69f283f6c4c5e.js" type="module"></script><script src="main-es2015.5a9aa16433404c3f423a.js" type="module"></script><script src="main-es5.5a9aa16433404c3f423a.js" nomodule="" defer=""></script></body>
117
117
 
118
118
  </html>