stoobly-agent 1.7.0__py3-none-any.whl → 1.7.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.
stoobly_agent/__init__.py CHANGED
@@ -1,2 +1,2 @@
1
1
  COMMAND = 'stoobly-agent'
2
- VERSION = '1.7.0'
2
+ VERSION = '1.7.1'
@@ -15,21 +15,21 @@ class HostsFileManager():
15
15
  ip_address: str
16
16
  hostnames: list[str]
17
17
 
18
- def __get_hosts_file_path(self) -> str:
18
+ # Split IP address and hostnames. Don't include inline comments
19
+ def __split_hosts_line(self, line: str) -> list[str]:
20
+ ip_addr_hosts_split = line.split('#')[0].split()
21
+ return ip_addr_hosts_split
22
+
23
+ def get_hosts_file_path(self) -> str:
19
24
  file_path = '/etc/hosts'
20
25
  if not os.path.exists(file_path):
21
26
  print(f"Error: File {file_path} not found.", file=sys.stderr)
22
27
  sys.exit(1)
23
28
  return file_path
24
29
 
25
- # Split IP address and hostnames. Don't include inline comments
26
- def __split_hosts_line(self, line: str) -> list[str]:
27
- ip_addr_hosts_split = line.split('#')[0].split()
28
- return ip_addr_hosts_split
29
-
30
30
  # Parses hosts file and returns a mapping of IP address to hostnames in a list.
31
31
  def get_hosts(self) -> list[IpAddressToHostnames]:
32
- hosts_file_path = self.__get_hosts_file_path()
32
+ hosts_file_path = self.get_hosts_file_path()
33
33
 
34
34
  if not hosts_file_path:
35
35
  return []
@@ -64,14 +64,14 @@ class HostsFileManager():
64
64
  return None
65
65
 
66
66
  def install_hostnames(self, hostnames: list[str]) -> None:
67
- hosts_file_path = self.__get_hosts_file_path()
67
+ hosts_file_path = self.get_hosts_file_path()
68
68
 
69
69
  self.__add_lines_between_markers(
70
70
  hosts_file_path, SCAFFOLD_HOSTS_DELIMITTER_BEGIN, SCAFFOLD_HOSTS_DELIMITTER_END, hostnames
71
71
  )
72
72
 
73
73
  def uninstall_hostnames(self, hostnames: list[str] = []) -> None:
74
- hosts_file_path = self.__get_hosts_file_path()
74
+ hosts_file_path = self.get_hosts_file_path()
75
75
 
76
76
  self.__remove_lines_between_markers(
77
77
  hosts_file_path, SCAFFOLD_HOSTS_DELIMITTER_BEGIN, SCAFFOLD_HOSTS_DELIMITTER_END, hostnames
@@ -182,4 +182,4 @@ class HostsFileManager():
182
182
  filtered_lines.append(line)
183
183
 
184
184
  with open(file_path, "w") as file:
185
- file.writelines(filtered_lines)
185
+ file.writelines(filtered_lines)
@@ -4,6 +4,7 @@ import socket
4
4
  import time
5
5
 
6
6
  from collections import Counter
7
+ from docker import errors as docker_errors
7
8
  from pathlib import Path
8
9
 
9
10
  import yaml
@@ -25,6 +26,7 @@ from stoobly_agent.app.cli.scaffold.service_docker_compose import ServiceDockerC
25
26
  from stoobly_agent.app.cli.scaffold.validate_command import ValidateCommand
26
27
  from stoobly_agent.app.cli.scaffold.validate_exceptions import ScaffoldValidateException
27
28
  from stoobly_agent.config.data_dir import DATA_DIR_NAME
29
+ from stoobly_agent.lib.logger import bcolors
28
30
 
29
31
  from .app import App
30
32
 
@@ -84,7 +86,10 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
84
86
  if attempt < retries - 1:
85
87
  time.sleep(delay)
86
88
 
87
- raise ScaffoldValidateException(f"Connection failed to hostname: {hostname}, port: {port}")
89
+ hostname_not_reachable_message = f"{bcolors.FAIL}Connection failed to hostname: {hostname}, port: {port}, num_retries: {retries}{bcolors.ENDC}"
90
+ suggestion_message = f"{bcolors.BOLD}Try confirming that {hostname} is reachable from your machine or environment.{bcolors.ENDC}"
91
+ error_message = f"{hostname_not_reachable_message}\n\n{suggestion_message}"
92
+ raise ScaffoldValidateException(error_message)
88
93
 
89
94
  # Check if hostname is defined in hosts file
90
95
  def hostname_exists(self, hostname: str) -> bool:
@@ -96,7 +101,12 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
96
101
  print(f"Correct hosts mapping found for {hostname}")
97
102
  return True
98
103
 
99
- raise ScaffoldValidateException(f"Missing hosts mapping for {hostname}")
104
+ hosts_file_path = hosts_file_manager.get_hosts_file_path()
105
+
106
+ missing_host_message = f"{bcolors.FAIL}Missing hosts mapping for {hostname}{bcolors.ENDC}"
107
+ suggestion_message = f"{bcolors.BOLD}Confirm hostname '{hostname}' is in your hosts file at '{hosts_file_path}'. If not there, please add it{bcolors.ENDC}"
108
+ error_message = f"{missing_host_message}\n\n{suggestion_message}"
109
+ raise ScaffoldValidateException(error_message)
100
110
 
101
111
  def validate_hostname(self, hostname: str, port: int) -> None:
102
112
  print(f"Validating hostname: {hostname}")
@@ -188,8 +198,13 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
188
198
 
189
199
  def validate_proxy_container(self, service_proxy_container: Container):
190
200
  print(f"Validating proxy container: {service_proxy_container.name}")
201
+
202
+ if service_proxy_container.status == 'exited' or service_proxy_container.attrs['State']['ExitCode'] != 0:
203
+ raise ScaffoldValidateException(f"Proxy container is exited: {service_proxy_container.name}")
204
+ if service_proxy_container.status != 'running':
205
+ raise ScaffoldValidateException(f"Proxy container is not running: {service_proxy_container.name}")
191
206
  if not service_proxy_container.attrs:
192
- raise ScaffoldValidateException(f"Container attributes are missing for: {container.name}")
207
+ raise ScaffoldValidateException(f"Container attributes are missing for: {service_proxy_container.name}")
193
208
 
194
209
  if not self.service_config.detached:
195
210
  self.validate_public_folder(service_proxy_container)
@@ -219,32 +234,53 @@ class ServiceWorkflowValidateCommand(ServiceCommand, ValidateCommand):
219
234
  self.validate_public_folder(init_container)
220
235
 
221
236
  if self.service_config.hostname:
222
- service_proxy_container = self.docker_client.containers.get(self.service_docker_compose.proxy_container_name)
237
+ try:
238
+ service_proxy_container = self.docker_client.containers.get(self.service_docker_compose.proxy_container_name)
239
+ except docker_errors.NotFound:
240
+ error_message = self._ValidateCommand__generate_container_not_found_error(self.service_docker_compose.proxy_container_name)
241
+ raise ScaffoldValidateException(error_message)
242
+
223
243
  self.validate_proxy_container(service_proxy_container)
224
244
 
245
+ # External services won't have a container to check
246
+ if self.is_local() or self.service_config.detached:
247
+ container_name = self.service_docker_compose.container_name
248
+ try:
249
+ service_container = self.docker_client.containers.get(container_name)
250
+ except docker_errors.NotFound:
251
+ error_message = self._ValidateCommand__generate_container_not_found_error(container_name)
252
+ raise ScaffoldValidateException(error_message)
253
+ if service_container.status == 'exited' or service_container.attrs['State']['ExitCode'] != 0:
254
+ raise ScaffoldValidateException(f"Custom container is exited: {service_container.name}")
255
+ if service_container.status != 'running':
256
+ raise ScaffoldValidateException(f"Custom container is not running: {service_container.name}")
257
+
225
258
  if self.is_local():
226
259
  print(f"Validating local user defined service: {self.service_name}")
227
260
  # Validate docker-compose path exists
228
261
  docker_compose_path = f"{self.app_dir_path}/{DATA_DIR_NAME}/docker/{self.service_docker_compose.service_name}/{self.workflow_name}/docker-compose.yml"
229
262
  destination_path = Path(docker_compose_path)
263
+
264
+ if not destination_path.exists():
265
+ message = f"{bcolors.FAIL}Docker Compose file does not exist: {destination_path}{bcolors.ENDC}"
266
+ suggestion_message = f"{bcolors.BOLD}A missing Docker Compose file often means it got deleted by accident. Please restore from backup or rerun the scaffold service create command.{bcolors.ENDC}"
267
+ error_message = f"{message}\n\n{suggestion_message}"
268
+ raise ScaffoldValidateException(error_message)
230
269
  if not destination_path.is_file():
231
270
  raise ScaffoldValidateException(f"Docker compose path is not a file: {destination_path}")
232
271
 
233
272
  # Validate docker-compose.yml file has the service defined
234
273
  with open(destination_path) as f:
235
274
  if self.service_name not in f.read():
236
- raise ScaffoldValidateException(f"Local service is not defined in Docker Compose file: {destination_path}")
237
-
238
- service_container = self.docker_client.containers.get(self.service_docker_compose.container_name)
239
- if service_container.status == 'exited':
240
- return False
275
+ message = f"{bcolors.FAIL}Custom container service is not defined in Docker Compose file: {destination_path}{bcolors.ENDC}"
276
+ suggestion_message = f"{bcolors.BOLD}Please add your service definition. See https://docs.docker.com/reference/compose-file/services {bcolors.ENDC}"
277
+ error_message = f"{message}\n\n{suggestion_message}"
278
+ raise ScaffoldValidateException(error_message)
241
279
 
242
280
  if self.service_config.detached:
243
- service_container = self.docker_client.containers.get(self.service_docker_compose.container_name)
244
281
  self.validate_detached(service_container)
245
282
 
246
- print(f"Done validating service: {self.service_name}, success!")
247
- print()
283
+ print(f"{bcolors.OKGREEN}✔ Done validating service: {self.service_name}, success!{bcolors.ENDC}\n")
248
284
 
249
285
  return True
250
286
 
@@ -1,4 +1,4 @@
1
- FROM stoobly/agent:1.6
1
+ FROM stoobly/agent:1.7
2
2
 
3
3
  ARG USER_ID
4
4
 
@@ -1,6 +1,7 @@
1
1
  import pdb
2
2
  import re
3
3
  from time import sleep
4
+ from typing import Union
4
5
 
5
6
  import docker
6
7
  from docker import errors as docker_errors
@@ -8,14 +9,22 @@ from docker.models.containers import Container
8
9
 
9
10
  from stoobly_agent.app.cli.scaffold.validate_exceptions import ScaffoldValidateException
10
11
  from stoobly_agent.config.data_dir import DATA_DIR_NAME
12
+ from stoobly_agent.lib.logger import bcolors
11
13
 
12
14
 
13
15
  class ValidateCommand():
14
16
  def __init__(self):
15
17
  self.docker_client = docker.from_env()
16
18
 
19
+ def __generate_container_not_found_error(self, container_name: Union[str, None]) -> str:
20
+ not_found_error_message = f"{bcolors.FAIL}Container not found: {container_name}{bcolors.ENDC}"
21
+ suggestion_message = f"{bcolors.BOLD}Run 'docker ps -a | grep {container_name}'. If found, then inspect the logs with 'docker logs {container_name}'{bcolors.ENDC}"
22
+ error_message = f"{not_found_error_message}\n\n{suggestion_message}"
23
+
24
+ return error_message
25
+
17
26
  # Some containers like init and configure can take longer than expected to finish so retry
18
- def __get_container(self, container_name: str) -> Container:
27
+ def __get_container_with_retries(self, container_name: str) -> Container:
19
28
  tries = 30
20
29
  for _ in range(tries):
21
30
  try:
@@ -24,20 +33,28 @@ class ValidateCommand():
24
33
  except docker_errors.NotFound:
25
34
  sleep(0.5)
26
35
 
27
- raise ScaffoldValidateException(f"Container not found: {container_name}")
36
+ error_message = self.__generate_container_not_found_error(container_name)
37
+ raise ScaffoldValidateException(error_message)
28
38
 
29
39
  def validate_init_containers(self, init_container_name, configure_container_name) -> None:
30
40
  print(f"Validating setup containers: {init_container_name}, {configure_container_name}")
31
41
 
32
-
33
- init_container = self.__get_container(init_container_name)
42
+ init_container = self.__get_container_with_retries(init_container_name)
34
43
  logs = init_container.logs()
44
+
35
45
  if logs and re.search('error', str(logs), re.IGNORECASE):
36
- raise ScaffoldValidateException(f"Error logs potentially detected in: {init_container_name}")
46
+ error_found_message = f"{bcolors.FAIL}Error logs potentially detected in: {init_container_name}{bcolors.ENDC}"
47
+ suggestion_message = f"{bcolors.BOLD}Run 'docker logs {init_container_name}'{bcolors.ENDC}"
48
+ error_message = f"{error_found_message}\n\n{suggestion_message}"
49
+ raise ScaffoldValidateException(error_message)
50
+
37
51
  if init_container.status != 'exited' or init_container.attrs['State']['ExitCode'] != 0:
38
- raise ScaffoldValidateException(f"init container {init_container_name} exited with: {init_container.attrs['State']['ExitCode']}")
52
+ init_exit_message = f"{bcolors.FAIL}init container {init_container_name} exited with: {init_container.attrs['State']['ExitCode']}{bcolors.ENDC}"
53
+ suggestion_message = f"{bcolors.BOLD}Run 'docker logs {init_container_name}'{bcolors.ENDC}"
54
+ error_message = f"{init_exit_message}\n\n{suggestion_message}"
55
+ raise ScaffoldValidateException(error_message)
39
56
 
40
- configure_container = self.__get_container(configure_container_name)
57
+ configure_container = self.__get_container_with_retries(configure_container_name)
41
58
 
42
59
  configure_container_ran = False
43
60
  if configure_container.status == 'exited' and configure_container.attrs['State']['ExitCode'] == 0:
@@ -49,12 +66,17 @@ class ValidateCommand():
49
66
  print(f"Validating detached for: {container.name}")
50
67
 
51
68
  if not container.attrs:
52
- raise ScaffoldValidateException(f"Container is missing: {container.name}")
69
+ error_message = self.__generate_container_not_found_error(container.name)
70
+ raise ScaffoldValidateException(error_message)
53
71
 
54
72
  volume_mounts = container.attrs['Mounts']
55
73
  for volume_mount in volume_mounts:
56
74
  if DATA_DIR_NAME in volume_mount['Source']:
57
75
  return
76
+
77
+ message = f"{bcolors.FAIL}Data directory is missing from container: {container.name}{bcolors.ENDC}"
78
+ suggestion_message = f"{bcolors.BOLD}Data directory might have failed to mount{bcolors.ENDC}"
79
+ error_message = f"{message}\n\n{suggestion_message}"
58
80
 
59
- raise ScaffoldValidateException(f"Data directory is missing from container: {container.name}")
81
+ raise ScaffoldValidateException(error_message)
60
82
 
@@ -15,6 +15,7 @@ from stoobly_agent.app.cli.scaffold.templates.constants import (
15
15
  from stoobly_agent.app.cli.scaffold.validate_command import ValidateCommand
16
16
  from stoobly_agent.app.cli.scaffold.validate_exceptions import ScaffoldValidateException
17
17
  from stoobly_agent.app.cli.scaffold.workflow_command import WorkflowCommand
18
+ from stoobly_agent.lib.logger import bcolors
18
19
 
19
20
  from .app import App
20
21
 
@@ -25,62 +26,86 @@ class WorkflowValidateCommand(WorkflowCommand, ValidateCommand):
25
26
  ValidateCommand.__init__(self)
26
27
  self.managed_services_docker_compose = ManagedServicesDockerCompose(target_workflow_name=self.workflow_name)
27
28
 
28
- def validate_core_components(self):
29
- print(f"Validating core component: {CORE_GATEWAY_SERVICE_NAME}")
29
+ # Gateway core service runs in all workflows
30
+ def validate_gateway_service(self):
31
+ print(f"Validating core service: {CORE_GATEWAY_SERVICE_NAME}")
30
32
  gateway_container_name = self.managed_services_docker_compose.gateway_container_name
31
- gateway_container = self.docker_client.containers.get(gateway_container_name)
32
- if not gateway_container or (gateway_container.status != 'running'):
33
- raise ScaffoldValidateException(f"Container '{gateway_container_name}' not found for service '{CORE_GATEWAY_SERVICE_NAME}'")
34
33
 
35
- print(f"Validating core component: {CORE_MOCK_UI_SERVICE_NAME}")
36
- mock_ui_container_name = self.managed_services_docker_compose.mock_ui_container_name
37
- mock_ui_container = self.docker_client.containers.get(mock_ui_container_name)
38
- if not mock_ui_container or (mock_ui_container.status != 'running'):
39
- raise ScaffoldValidateException(f"Container '{mock_ui_container_name}' not found for service '{CORE_MOCK_UI_SERVICE_NAME}'")
34
+ container_missing_message = f"{bcolors.FAIL}Container '{gateway_container_name}' not found for service '{CORE_GATEWAY_SERVICE_NAME}'{bcolors.ENDC}"
35
+ suggestion_message = f"{bcolors.BOLD}Workflow might not be running yet. Try running 'scaffold workflow up <WORKFLOW_NAME>'{bcolors.ENDC}"
36
+ error_message = f"{container_missing_message}\n\n{suggestion_message}"
40
37
 
41
- def validate_no_core_components(self):
42
38
  try:
43
- core_mock_ui_container_name = self.docker_client.containers.get(self.managed_services_docker_compose.mock_ui_container_name)
44
- if core_mock_ui_container_name:
45
- raise ScaffoldValidateException(f"Stoobly UI container is running when it shouldn't: {core_mock_ui_container_name.name}")
39
+ gateway_container = self.docker_client.containers.get(gateway_container_name)
40
+ if not gateway_container or (gateway_container.status != 'running'):
41
+ raise ScaffoldValidateException(error_message)
46
42
  except docker_errors.NotFound:
47
- pass
48
-
49
- print(f"Skipping validating core component: {CORE_GATEWAY_SERVICE_NAME}")
50
- print(f"Skipping validating core component: {CORE_MOCK_UI_SERVICE_NAME}")
43
+ raise ScaffoldValidateException(error_message)
51
44
 
52
-
53
- def validate(self) -> bool:
54
- print(f"Validating workflow: {self.workflow_name}")
55
- print(f"Validating core components: {CORE_SERVICES}")
45
+ def validate_mock_ui_service(self):
46
+ mock_ui_container_name = self.managed_services_docker_compose.mock_ui_container_name
56
47
 
48
+ # The stoobly-ui service does not run in test workflows
57
49
  if self.workflow_name == WORKFLOW_TEST_TYPE:
58
- # Don't validate the gateway and mock_ui core components in the "test" workflow
59
- self.validate_no_core_components()
60
- else:
61
- self.validate_core_components()
50
+ try:
51
+ mock_ui_container = self.docker_client.containers.get(mock_ui_container_name)
52
+
53
+ if mock_ui_container:
54
+ ui_found_message = f"{bcolors.FAIL}Stoobly UI container is running when it shouldn't: {mock_ui_container_name}{bcolors.ENDC}"
55
+ suggestion_message = f"{bcolors.BOLD}Run 'docker ps | grep stoobly_ui' to check if the Stoobly UI is running during the test workflow. Did you stop the record or mock workflow yet?{bcolors.ENDC}"
56
+ error_message = f"{ui_found_message}\n\n{suggestion_message}"
57
+ raise ScaffoldValidateException(error_message)
58
+ except docker_errors.NotFound:
59
+ print(f"Skipping validating core service: {CORE_MOCK_UI_SERVICE_NAME}")
60
+ return
61
+
62
+ print(f"Validating core service: {CORE_MOCK_UI_SERVICE_NAME}")
63
+ container_missing_message = f"{bcolors.FAIL}Container '{mock_ui_container_name}' not found for service '{CORE_MOCK_UI_SERVICE_NAME}'{bcolors.ENDC}"
64
+ suggestion_message = f"{bcolors.BOLD}Stoobly UI is not running. Check if the container is up with 'docker ps -a | grep {mock_ui_container_name}'{bcolors.ENDC}"
65
+ mock_ui_missing_error_message = f"{container_missing_message}\n\n{suggestion_message}"
62
66
 
63
- self.validate_init_containers(self.managed_services_docker_compose.init_container_name, self.managed_services_docker_compose.configure_container_name)
67
+ try:
68
+ mock_ui_container = self.docker_client.containers.get(mock_ui_container_name)
69
+ if not mock_ui_container or (mock_ui_container.status != 'running'):
70
+ raise ScaffoldValidateException(mock_ui_missing_error_message)
71
+ except docker_errors.NotFound:
72
+ raise ScaffoldValidateException(mock_ui_missing_error_message)
64
73
 
65
- print(f"Validating core component: {CORE_ENTRYPOINT_SERVICE_NAME}")
74
+ def validate_entrypoint_service(self):
75
+ print(f"Validating core service: {CORE_ENTRYPOINT_SERVICE_NAME}")
66
76
 
77
+ core_entrypoint_init_container_name = None
67
78
  try:
68
79
  core_entrypoint_init_container_name = self.managed_services_docker_compose.entrypoint_init_container_name
69
80
  entrypoint_init_container = self.docker_client.containers.get(core_entrypoint_init_container_name)
70
81
  except docker_errors.NotFound:
71
- raise ScaffoldValidateException(f"Container not found: {core_entrypoint_init_container_name}")
72
-
82
+ error_message = self._ValidateCommand__generate_container_not_found_error(core_entrypoint_init_container_name)
83
+ raise ScaffoldValidateException(error_message)
84
+
85
+ core_entrypoint_configure_container_name = None
73
86
  try:
74
87
  core_entrypoint_configure_container_name = self.managed_services_docker_compose.entrypoint_configure_container_name
75
88
  entrypoint_configure_container = self.docker_client.containers.get(core_entrypoint_configure_container_name)
76
89
  except docker_errors.NotFound:
77
- raise ScaffoldValidateException(f"Container not found: {core_entrypoint_configure_container_name}")
90
+ error_message = self._ValidateCommand__generate_container_not_found_error(core_entrypoint_configure_container_name)
91
+ raise ScaffoldValidateException(error_message)
78
92
 
79
93
  # NOTE: we should check the correct workflow mode is enabled one day
80
94
  # That's not currently queryable
81
95
 
82
- print(f"Done validating workflow: {self.workflow_name}, success!")
83
- print()
96
+ print(f"{bcolors.OKGREEN}✔ Done validating core services for workflow: {self.workflow_name}, success!\n{bcolors.ENDC}")
97
+
98
+ def validate_core_services(self):
99
+ self.validate_gateway_service()
100
+ self.validate_mock_ui_service()
101
+ self.validate_init_containers(self.managed_services_docker_compose.init_container_name, self.managed_services_docker_compose.configure_container_name)
102
+ self.validate_entrypoint_service()
103
+
104
+ def validate(self) -> bool:
105
+ print(f"Validating workflow: {self.workflow_name}\n")
106
+ print(f"Validating core services: {CORE_SERVICES}")
107
+
108
+ self.validate_core_services()
84
109
 
85
110
  return True
86
111
 
@@ -518,6 +518,7 @@ def up(**kwargs):
518
518
 
519
519
  __run_script(script, kwargs['dry_run'])
520
520
 
521
+
521
522
  @workflow.command(
522
523
  help="Validate a scaffold workflow"
523
524
  )
@@ -536,7 +537,8 @@ def validate(**kwargs):
536
537
  command = WorkflowValidateCommand(app, **config)
537
538
  command.validate()
538
539
  except ScaffoldValidateException as sve:
539
- print(f"\nFatal Scaffold Validation Exception: {sve}", file=sys.stderr)
540
+ print(f"{bcolors.FAIL}\nFatal scaffold validation exception:{bcolors.ENDC}\n{sve}", file=sys.stderr)
541
+ print("\nSee the scaffold workflow troubleshooting guide at: https://docs.stoobly.com/guides/how-to-integrate-e2e-testing/how-to-run-a-workflow/troubleshooting", file=sys.stderr)
540
542
  sys.exit(1)
541
543
 
542
544
  try:
@@ -546,9 +548,12 @@ def validate(**kwargs):
546
548
  command = ServiceWorkflowValidateCommand(app, **config)
547
549
  command.validate()
548
550
  except ScaffoldValidateException as sve:
549
- print(f"\nFatal Scaffold Validation Exception: {sve}", file=sys.stderr)
551
+ print(f"{bcolors.FAIL}\nFatal scaffold validation exception:{bcolors.ENDC}\n{sve}", file=sys.stderr)
552
+ print("\nSee the scaffold workflow troubleshooting guide at: https://docs.stoobly.com/guides/how-to-integrate-e2e-testing/how-to-run-a-workflow/troubleshooting", file=sys.stderr)
550
553
  sys.exit(1)
551
554
 
555
+ print(f"{bcolors.OKCYAN}✔ Done validating Stoobly scaffold and services, success!{bcolors.ENDC}")
556
+
552
557
  @hostname.command(
553
558
  help="Update the system hosts file for all scaffold service hostnames"
554
559
  )
@@ -121,7 +121,7 @@ class TestScaffoldE2e():
121
121
  # Validate docker-compose path exists
122
122
  destination_path = Path(local_service_docker_compose.docker_compose_path)
123
123
  assert destination_path.is_file()
124
- # Add user defined Docker Compose file for the local service
124
+ # Add user defined Docker Compose file for the custom container service
125
125
  shutil.copyfile(local_service_mock_docker_compose_path, destination_path)
126
126
 
127
127
  # Record workflow doesn't have a public folder
@@ -137,7 +137,7 @@ class TestScaffoldE2e():
137
137
  ScaffoldCliInvoker.cli_workflow_down(runner, app_dir_path, target_workflow_name)
138
138
  shutil.rmtree(app_dir_path)
139
139
 
140
- def test_core_components(self, app_dir_path, target_workflow_name):
140
+ def test_core_services(self, app_dir_path, target_workflow_name):
141
141
  app = App(app_dir_path, DOCKER_NAMESPACE)
142
142
  config = {
143
143
  'workflow_name': target_workflow_name,
@@ -260,7 +260,7 @@ class TestScaffoldE2e():
260
260
  ScaffoldCliInvoker.cli_workflow_down(runner, app_dir_path, target_workflow_name)
261
261
  shutil.rmtree(app_dir_path)
262
262
 
263
- def test_no_core_components(self, app_dir_path, target_workflow_name):
263
+ def test_no_core_services(self, app_dir_path, target_workflow_name):
264
264
  app = App(app_dir_path, DOCKER_NAMESPACE)
265
265
  config = {
266
266
  'workflow_name': target_workflow_name,
@@ -11,7 +11,7 @@ class TestHostsFileManager():
11
11
  yield HostsFileManager()
12
12
 
13
13
  def test_get_hosts_file_path(self, hosts_file_manager):
14
- hosts_file_path = hosts_file_manager._HostsFileManager__get_hosts_file_path()
14
+ hosts_file_path = hosts_file_manager.get_hosts_file_path()
15
15
 
16
16
  # Test runners are all Linux distros for now
17
17
  assert hosts_file_path == '/etc/hosts'
@@ -1 +1 @@
1
- 1.7.0
1
+ 1.7.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: stoobly-agent
3
- Version: 1.7.0
3
+ Version: 1.7.1
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=zQauQ1qcCYeR8Q0sZDvwYQ0y7_-8jFQeYex6fFUoY4M,44
1
+ stoobly_agent/__init__.py,sha256=lW3HNRv30I4AZM5G54ri20nFb4CRG36fCUHauTdkLFQ,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=Vvz53yB0bR7J-QqMAkLlhcZrA4P64ZEN7w8cMbgl6o0,5261
@@ -92,7 +92,7 @@ stoobly_agent/app/cli/scaffold/docker/workflow/dns_decorator.py,sha256=nlDPbyF1h
92
92
  stoobly_agent/app/cli/scaffold/docker/workflow/mock_decorator.py,sha256=DcBnwA8YhE-VdrUiWf2xPcqirEMZEQm3AwssAg9CxLo,1226
93
93
  stoobly_agent/app/cli/scaffold/docker/workflow/reverse_proxy_decorator.py,sha256=zD4FEtBMiPEtvEb798LOfFH8sJhTWQYpA9THLq5UMbg,1227
94
94
  stoobly_agent/app/cli/scaffold/env.py,sha256=e-Ve4p3RUgzFx22B3SIYttvJ_yLuDtA27oDACZ8n-6E,1140
95
- stoobly_agent/app/cli/scaffold/hosts_file_manager.py,sha256=FiX1hYEWN4cJiCOV4h6wOOlY7t71uwIwe6t2upS65aQ,5006
95
+ stoobly_agent/app/cli/scaffold/hosts_file_manager.py,sha256=zNX5wh6zXQ4J2BA0YYdD7_CPqDz02b_ghXsY3oTjjB4,4999
96
96
  stoobly_agent/app/cli/scaffold/managed_services_docker_compose.py,sha256=-wLBXUi7DCWsfm5KzZzd_kdJKOTl1NT924XR7dyjbSY,574
97
97
  stoobly_agent/app/cli/scaffold/service.py,sha256=L9K6QE0k5KSEC8_fSwtdwwTSO_DsIpqSPW-AG7Bg76o,501
98
98
  stoobly_agent/app/cli/scaffold/service_command.py,sha256=9kIKiFC5Jo425VWYD4NDvUOdMP-pNyq2D5Ip1ZAPj3A,1054
@@ -101,9 +101,9 @@ stoobly_agent/app/cli/scaffold/service_create_command.py,sha256=1B6TK3JDAjouikCV
101
101
  stoobly_agent/app/cli/scaffold/service_delete_command.py,sha256=_nBDQjm8eL62MQpzSCxgUHlW04ZXKG8MDlN1BXxlqww,986
102
102
  stoobly_agent/app/cli/scaffold/service_docker_compose.py,sha256=OMUN1-ujQYIZXxDvS4XBf5C9wGalQULkwOiBBQPZbHY,820
103
103
  stoobly_agent/app/cli/scaffold/service_workflow.py,sha256=sQ_Edy_wGHKMXpD0DmhnOWkGEKz7gSgEGNI8f7aXOdg,444
104
- stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py,sha256=M5fc9RiCgN9ReCXsb3KIVJIKo0aSIOv28Q3izLkEQTo,10036
104
+ stoobly_agent/app/cli/scaffold/service_workflow_validate_command.py,sha256=RxPJollW_FQjlPcIbvqGRVH2adJYPcLNCsVMpW5wjxs,12740
105
105
  stoobly_agent/app/cli/scaffold/templates/__init__.py,sha256=p-23uLU2QGksJASDD13soldc0vr_AOS3QdGlSnbs3bM,167
106
- stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context,sha256=xnUFSlP8YvEs04lTh1Jj_ggNcwyVjyzZjzF0Vh9pris,158
106
+ stoobly_agent/app/cli/scaffold/templates/app/.Dockerfile.context,sha256=AK8j4JWDmQxfsOX-Ysp-LNckPGhp-tiDOdPMdYItmso,158
107
107
  stoobly_agent/app/cli/scaffold/templates/app/.Makefile,sha256=hree9QL8xl7v_Yzv40bcO4-ZDlyA4Qnm-VWU2dnMuXQ,8955
108
108
  stoobly_agent/app/cli/scaffold/templates/app/.docker-compose.base.yml,sha256=6tFqXh3ine8vaD0FCL5TMoY5NjKx2wLUR8XpW3tJtew,245
109
109
  stoobly_agent/app/cli/scaffold/templates/app/.docker-compose.networks.yml,sha256=I4PbJpQjFHb5IbAUWNvYM6okDEtmwtKFDQg-yog05WM,141
@@ -192,7 +192,7 @@ stoobly_agent/app/cli/scaffold/templates/workflow/test/bin/init,sha256=EaoFDyoJb
192
192
  stoobly_agent/app/cli/scaffold/templates/workflow/test/fixtures.yml,sha256=CJlZ_kugygZpmyqIauBjNZxqk7XyLaa3yl3AWj8KV28,259
193
193
  stoobly_agent/app/cli/scaffold/templates/workflow/test/lifecycle_hooks.py,sha256=U7mlzT_wBR3uhHSG6CAyt5tBUNAvdIrCw33gdB-F294,467
194
194
  stoobly_agent/app/cli/scaffold/templates/workflow/test/public/.gitignore,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
195
- stoobly_agent/app/cli/scaffold/validate_command.py,sha256=OjAbX-YNx165od6HCwKmgdyF3upNdyQsL_vXTsOK2mo,2426
195
+ stoobly_agent/app/cli/scaffold/validate_command.py,sha256=BEDcF5qPDbMjWe-SCAr5jxcrVMZlV3ciLa89TDDENmM,3787
196
196
  stoobly_agent/app/cli/scaffold/validate_exceptions.py,sha256=Jtjl4OkbbSRWm0hy7Kf_50zcFh2J324HhNcnwJKH_Oc,54
197
197
  stoobly_agent/app/cli/scaffold/workflow.py,sha256=KlbWT9CIo9EpZxKU1WVZtmodhxK7CpmLUHPNk4Mh6DA,1570
198
198
  stoobly_agent/app/cli/scaffold/workflow_command.py,sha256=eI9I5LLgO0U3b46QhHusy-4BV2zUDVai6jErcluYCRI,3344
@@ -202,8 +202,8 @@ stoobly_agent/app/cli/scaffold/workflow_create_command.py,sha256=R3L1obUTidGngbI
202
202
  stoobly_agent/app/cli/scaffold/workflow_env.py,sha256=x8V5pJmIiklD3f2q2-qq-CORf4YaXYq_r2JpR2MmSwk,416
203
203
  stoobly_agent/app/cli/scaffold/workflow_log_command.py,sha256=Bke4lMOMxuDUFuAx9nlXHbKgYMO4KAg9ASHvjz4aVWc,1372
204
204
  stoobly_agent/app/cli/scaffold/workflow_run_command.py,sha256=eF3aaK4OIZXYuSBEAeBnhAL7EZrS1G4mSYrJbEiXt2o,11082
205
- stoobly_agent/app/cli/scaffold/workflow_validate_command.py,sha256=wv1Zrq0GoYO8ryac3oGno0C282c6UjMtniO19mBSyrE,4036
206
- stoobly_agent/app/cli/scaffold_cli.py,sha256=kD_2YbtdcMjFzJ2u8lBchL5vo0ey4_a_HXmE3V3Quuw,28607
205
+ stoobly_agent/app/cli/scaffold/workflow_validate_command.py,sha256=Uo_yo6rVR1ZR7xpvsQvlH48AyMBVLRupd4G-bRjzm_Q,5584
206
+ stoobly_agent/app/cli/scaffold_cli.py,sha256=FfI_v_Rpzoieou8W3l640zUrmGSAnBDSdp_h4ND8tGU,29135
207
207
  stoobly_agent/app/cli/scenario_cli.py,sha256=3J1EiJOvunkfWrEkOsanw-XrKkOk78ij_GjBlE9p7CE,8229
208
208
  stoobly_agent/app/cli/snapshot_cli.py,sha256=cpCjxFYBuVwLuq_b2lIUu-5zWqupRlrp4xWgDytirSM,10047
209
209
  stoobly_agent/app/cli/trace_cli.py,sha256=K7E-vx3JUcqEDSWOdIOi_AieKNQz7dBfmRrVvKDkzFI,4605
@@ -677,8 +677,8 @@ stoobly_agent/test/app/cli/request/request_snapshot_test.py,sha256=0013aoiMZin-2
677
677
  stoobly_agent/test/app/cli/request/request_test_test.py,sha256=-cJNXKjgryVVfVt-7IN5fIhBwe3NjFoPmeavDH8lAjU,5527
678
678
  stoobly_agent/test/app/cli/scaffold/cli_invoker.py,sha256=_nGDLUsYxqkeqs5DdhvAeXy3IuotpgqKHXKVzu6GDF4,3700
679
679
  stoobly_agent/test/app/cli/scaffold/cli_test.py,sha256=sMNvO845MIu5DVGa1HmwXQDmKDcwrfNTdEb3fK5886w,4557
680
- stoobly_agent/test/app/cli/scaffold/e2e_test.py,sha256=Zq0nYPObbH6tYjuGHrT2hOT71FO6kDnvRQWGDK1yK4M,12971
681
- stoobly_agent/test/app/cli/scaffold/hosts_file_manager_test.py,sha256=sVFHHYBxzyfRbVIe3gnToQ-6JPy71BCBjdQDnboYzac,2306
680
+ stoobly_agent/test/app/cli/scaffold/e2e_test.py,sha256=IGWT0EXrMtB8i8kdLFbN7O8NvLrJYTi-iQ_GRiUoA94,12978
681
+ stoobly_agent/test/app/cli/scaffold/hosts_file_manager_test.py,sha256=ztcPh1x0ZCW1FWA5YL4ulEVjfbW9TOPgk1bnSDPNmCw,2287
682
682
  stoobly_agent/test/app/cli/scenario/scenario_create_test.py,sha256=fGqcjO1_1OvdpUMQfGRVkSyFe61u8WIcp_ndLFrf33A,3962
683
683
  stoobly_agent/test/app/cli/scenario/scenario_replay_integration_test.py,sha256=NbGJzmvPsNLBR0ac65yt_cOTfpnsST1IG7i3F0euwAk,7031
684
684
  stoobly_agent/test/app/cli/scenario/scenario_replay_test.py,sha256=XqR-GDrR8uUtxvukBuIFJ4GsOzBj9WWi4b0VKmfhFyE,6874
@@ -703,7 +703,7 @@ stoobly_agent/test/app/models/factories/resource/local_db/helpers/log_test.py,sh
703
703
  stoobly_agent/test/app/models/factories/resource/local_db/helpers/tiebreak_scenario_request_test.py,sha256=a1SFLyEyRRLuADvAw6ckQQKORFXvyK1lyrbkaLWx8oU,3399
704
704
  stoobly_agent/test/app/models/factories/resource/local_db/request_adapter_test.py,sha256=Pzq1cBPnP9oSWG-p0c-VoymoHxgp483QmNwmV1b78RA,8453
705
705
  stoobly_agent/test/app/models/factories/resource/local_db/response_adapter_test.py,sha256=9P95EKH5rZGOrmRkRIDlQZqtiLJHk9735og18Ffwpfw,2204
706
- stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION,sha256=LBlAk30Td33A8sWokJIf8T6YuRqenUQ6QmUdjQ2LRI0,5
706
+ stoobly_agent/test/app/models/schemas/.stoobly/db/VERSION,sha256=u3Mg2DHnoVGqkBw15zJsdS-i71Ak8wdoxCMZuL7Rce0,6
707
707
  stoobly_agent/test/app/models/schemas/.stoobly/db/stoobly_agent.sqlite3,sha256=ch8gNx6zIelLKQx65gwFx_LRNqUD3EC5xcHZ0ukIQiU,188416
708
708
  stoobly_agent/test/app/models/schemas/.stoobly/settings.yml,sha256=vLwMjweKOdod6tSLtIlyBefPQuNXq9wio4kBaODKtAU,726
709
709
  stoobly_agent/test/app/models/schemas/.stoobly/tmp/options.json,sha256=OTRzarwus48CTrItedXCrgQttJHSEZonEYc7R_knvYg,2212
@@ -744,8 +744,8 @@ stoobly_agent/test/mock_data/scaffold/docker-compose-local-service.yml,sha256=1W
744
744
  stoobly_agent/test/mock_data/scaffold/index.html,sha256=qJwuYajKZ4ihWZrJQ3BNObV5kf1VGnnm_vqlPJzdqLE,258
745
745
  stoobly_agent/test/mock_data/uspto.yaml,sha256=6U5se7C3o-86J4m9xpOk9Npias399f5CbfWzR87WKwE,7835
746
746
  stoobly_agent/test/test_helper.py,sha256=m_oAI7tmRYCNZdKfNqISWhMv3e44tjeYViQ3nTUfnos,1007
747
- stoobly_agent-1.7.0.dist-info/LICENSE,sha256=o93sj12cdoEOsTCjPaPFsw3Xq0SXs3pPcY-9reE2sEw,548
748
- stoobly_agent-1.7.0.dist-info/METADATA,sha256=81lb4fSP6TTy7PKZT_a2oILcBUvP94_wCxF40qOyw74,3087
749
- stoobly_agent-1.7.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
750
- stoobly_agent-1.7.0.dist-info/entry_points.txt,sha256=aq5wix5oC8MDQtmyPGU0xaFrsjJg7WH28NmXh2sc3Z8,56
751
- stoobly_agent-1.7.0.dist-info/RECORD,,
747
+ stoobly_agent-1.7.1.dist-info/LICENSE,sha256=o93sj12cdoEOsTCjPaPFsw3Xq0SXs3pPcY-9reE2sEw,548
748
+ stoobly_agent-1.7.1.dist-info/METADATA,sha256=ljJB5pxOKkbXohMwMwaOTX5_5rRkxDJYtX2-tzM3kgY,3087
749
+ stoobly_agent-1.7.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
750
+ stoobly_agent-1.7.1.dist-info/entry_points.txt,sha256=aq5wix5oC8MDQtmyPGU0xaFrsjJg7WH28NmXh2sc3Z8,56
751
+ stoobly_agent-1.7.1.dist-info/RECORD,,