devservices 1.0.8__tar.gz → 1.0.10__tar.gz

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 (59) hide show
  1. {devservices-1.0.8 → devservices-1.0.10}/PKG-INFO +2 -2
  2. {devservices-1.0.8 → devservices-1.0.10}/README.md +1 -1
  3. {devservices-1.0.8 → devservices-1.0.10}/devservices/commands/down.py +68 -15
  4. {devservices-1.0.8 → devservices-1.0.10}/devservices/commands/list_dependencies.py +7 -0
  5. {devservices-1.0.8 → devservices-1.0.10}/devservices/commands/list_services.py +16 -3
  6. {devservices-1.0.8 → devservices-1.0.10}/devservices/commands/logs.py +14 -2
  7. {devservices-1.0.8 → devservices-1.0.10}/devservices/commands/status.py +10 -1
  8. {devservices-1.0.8 → devservices-1.0.10}/devservices/commands/up.py +17 -3
  9. {devservices-1.0.8 → devservices-1.0.10}/devservices/configs/service_config.py +3 -1
  10. {devservices-1.0.8 → devservices-1.0.10}/devservices/exceptions.py +10 -4
  11. {devservices-1.0.8 → devservices-1.0.10}/devservices/utils/console.py +3 -0
  12. {devservices-1.0.8 → devservices-1.0.10}/devservices/utils/dependencies.py +121 -28
  13. {devservices-1.0.8 → devservices-1.0.10}/devservices/utils/docker_compose.py +29 -19
  14. {devservices-1.0.8 → devservices-1.0.10}/devservices/utils/services.py +8 -1
  15. {devservices-1.0.8 → devservices-1.0.10}/devservices/utils/state.py +48 -22
  16. {devservices-1.0.8 → devservices-1.0.10}/devservices.egg-info/PKG-INFO +2 -2
  17. {devservices-1.0.8 → devservices-1.0.10}/pyproject.toml +1 -1
  18. devservices-1.0.10/tests/commands/test_down.py +970 -0
  19. {devservices-1.0.8 → devservices-1.0.10}/tests/commands/test_list_dependencies.py +21 -0
  20. devservices-1.0.10/tests/commands/test_list_services.py +176 -0
  21. {devservices-1.0.8 → devservices-1.0.10}/tests/commands/test_logs.py +49 -9
  22. {devservices-1.0.8 → devservices-1.0.10}/tests/commands/test_purge.py +64 -27
  23. {devservices-1.0.8 → devservices-1.0.10}/tests/commands/test_status.py +21 -0
  24. {devservices-1.0.8 → devservices-1.0.10}/tests/commands/test_up.py +368 -217
  25. {devservices-1.0.8 → devservices-1.0.10}/tests/configs/test_service_config.py +1 -1
  26. {devservices-1.0.8 → devservices-1.0.10}/tests/utils/test_dependencies.py +619 -6
  27. {devservices-1.0.8 → devservices-1.0.10}/tests/utils/test_docker_compose.py +3 -9
  28. {devservices-1.0.8 → devservices-1.0.10}/tests/utils/test_services.py +65 -2
  29. devservices-1.0.10/tests/utils/test_state.py +84 -0
  30. devservices-1.0.8/tests/commands/test_down.py +0 -265
  31. devservices-1.0.8/tests/commands/test_list_services.py +0 -89
  32. devservices-1.0.8/tests/utils/test_state.py +0 -54
  33. {devservices-1.0.8 → devservices-1.0.10}/LICENSE.md +0 -0
  34. {devservices-1.0.8 → devservices-1.0.10}/devservices/__init__.py +0 -0
  35. {devservices-1.0.8 → devservices-1.0.10}/devservices/commands/__init__.py +0 -0
  36. {devservices-1.0.8 → devservices-1.0.10}/devservices/commands/purge.py +0 -0
  37. {devservices-1.0.8 → devservices-1.0.10}/devservices/commands/update.py +0 -0
  38. {devservices-1.0.8 → devservices-1.0.10}/devservices/constants.py +0 -0
  39. {devservices-1.0.8 → devservices-1.0.10}/devservices/main.py +0 -0
  40. {devservices-1.0.8 → devservices-1.0.10}/devservices/utils/__init__.py +0 -0
  41. {devservices-1.0.8 → devservices-1.0.10}/devservices/utils/check_for_update.py +0 -0
  42. {devservices-1.0.8 → devservices-1.0.10}/devservices/utils/devenv.py +0 -0
  43. {devservices-1.0.8 → devservices-1.0.10}/devservices/utils/docker.py +0 -0
  44. {devservices-1.0.8 → devservices-1.0.10}/devservices/utils/file_lock.py +0 -0
  45. {devservices-1.0.8 → devservices-1.0.10}/devservices/utils/install_binary.py +0 -0
  46. {devservices-1.0.8 → devservices-1.0.10}/devservices.egg-info/SOURCES.txt +0 -0
  47. {devservices-1.0.8 → devservices-1.0.10}/devservices.egg-info/dependency_links.txt +0 -0
  48. {devservices-1.0.8 → devservices-1.0.10}/devservices.egg-info/entry_points.txt +0 -0
  49. {devservices-1.0.8 → devservices-1.0.10}/devservices.egg-info/requires.txt +0 -0
  50. {devservices-1.0.8 → devservices-1.0.10}/devservices.egg-info/top_level.txt +0 -0
  51. {devservices-1.0.8 → devservices-1.0.10}/setup.cfg +0 -0
  52. {devservices-1.0.8 → devservices-1.0.10}/testing/__init__.py +0 -0
  53. {devservices-1.0.8 → devservices-1.0.10}/testing/utils.py +0 -0
  54. {devservices-1.0.8 → devservices-1.0.10}/tests/__init__.py +0 -0
  55. {devservices-1.0.8 → devservices-1.0.10}/tests/commands/test_update.py +0 -0
  56. {devservices-1.0.8 → devservices-1.0.10}/tests/conftest.py +0 -0
  57. {devservices-1.0.8 → devservices-1.0.10}/tests/utils/test_check_for_update.py +0 -0
  58. {devservices-1.0.8 → devservices-1.0.10}/tests/utils/test_docker.py +0 -0
  59. {devservices-1.0.8 → devservices-1.0.10}/tests/utils/test_install_binary.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: devservices
3
- Version: 1.0.8
3
+ Version: 1.0.10
4
4
  Requires-Python: >=3.10
5
5
  License-File: LICENSE.md
6
6
  Requires-Dist: pyyaml
@@ -11,7 +11,7 @@ A standalone cli tool used to manage dependencies for services. It simplifies th
11
11
  The recommended way to install devservices is through a virtualenv in the requirements.txt.
12
12
 
13
13
  ```
14
- devservices==1.0.8
14
+ devservices==1.0.10
15
15
  ```
16
16
 
17
17
 
@@ -15,11 +15,13 @@ from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
15
15
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
16
16
  from devservices.constants import DEVSERVICES_DIR_NAME
17
17
  from devservices.exceptions import ConfigError
18
+ from devservices.exceptions import ConfigNotFoundError
18
19
  from devservices.exceptions import DependencyError
19
20
  from devservices.exceptions import DockerComposeError
20
21
  from devservices.exceptions import ServiceNotFoundError
21
22
  from devservices.utils.console import Console
22
23
  from devservices.utils.console import Status
24
+ from devservices.utils.dependencies import construct_dependency_graph
23
25
  from devservices.utils.dependencies import get_non_shared_remote_dependencies
24
26
  from devservices.utils.dependencies import install_and_verify_dependencies
25
27
  from devservices.utils.dependencies import InstalledRemoteDependency
@@ -29,6 +31,7 @@ from devservices.utils.docker_compose import run_cmd
29
31
  from devservices.utils.services import find_matching_service
30
32
  from devservices.utils.services import Service
31
33
  from devservices.utils.state import State
34
+ from devservices.utils.state import StateTables
32
35
 
33
36
 
34
37
  def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
@@ -56,6 +59,12 @@ def down(args: Namespace) -> None:
56
59
  service_name = args.service_name
57
60
  try:
58
61
  service = find_matching_service(service_name)
62
+ except ConfigNotFoundError as e:
63
+ capture_exception(e)
64
+ console.failure(
65
+ f"{str(e)}. Please specify a service (i.e. `devservices down sentry`) or run the command from a directory with a devservices configuration."
66
+ )
67
+ exit(1)
59
68
  except ConfigError as e:
60
69
  capture_exception(e)
61
70
  console.failure(str(e))
@@ -67,12 +76,20 @@ def down(args: Namespace) -> None:
67
76
  modes = service.config.modes
68
77
 
69
78
  state = State()
70
- started_services = state.get_started_services()
71
- if service.name not in started_services:
79
+ starting_services = set(state.get_service_entries(StateTables.STARTING_SERVICES))
80
+ started_services = set(state.get_service_entries(StateTables.STARTED_SERVICES))
81
+ active_services = starting_services.union(started_services)
82
+ if service.name not in active_services:
72
83
  console.warning(f"{service.name} is not running")
73
84
  exit(0)
74
85
 
75
- active_modes = state.get_active_modes_for_service(service.name)
86
+ active_starting_modes = state.get_active_modes_for_service(
87
+ service.name, StateTables.STARTING_SERVICES
88
+ )
89
+ active_started_modes = state.get_active_modes_for_service(
90
+ service.name, StateTables.STARTED_SERVICES
91
+ )
92
+ active_modes = active_starting_modes or active_started_modes
76
93
  mode_dependencies = set()
77
94
  for active_mode in active_modes:
78
95
  active_mode_dependencies = modes.get(active_mode, [])
@@ -80,7 +97,6 @@ def down(args: Namespace) -> None:
80
97
 
81
98
  with Status(
82
99
  lambda: console.warning(f"Stopping {service.name}"),
83
- lambda: console.success(f"{service.name} stopped"),
84
100
  ) as status:
85
101
  try:
86
102
  remote_dependencies = install_and_verify_dependencies(
@@ -88,7 +104,9 @@ def down(args: Namespace) -> None:
88
104
  )
89
105
  except DependencyError as de:
90
106
  capture_exception(de)
91
- status.failure(str(de))
107
+ status.failure(
108
+ f"{str(de)}. If this error persists, try running `devservices purge`"
109
+ )
92
110
  exit(1)
93
111
  try:
94
112
  remote_dependencies = get_non_shared_remote_dependencies(
@@ -96,18 +114,53 @@ def down(args: Namespace) -> None:
96
114
  )
97
115
  except DependencyError as de:
98
116
  capture_exception(de)
99
- status.failure(str(de))
100
- exit(1)
101
- try:
102
- _down(service, remote_dependencies, list(mode_dependencies), status)
103
- except DockerComposeError as dce:
104
- capture_exception(dce)
105
- status.failure(f"Failed to stop {service.name}: {dce.stderr}")
117
+ status.failure(
118
+ f"{str(de)}. If this error persists, try running `devservices purge`"
119
+ )
106
120
  exit(1)
107
121
 
122
+ # Check if any service depends on the service we are trying to bring down
123
+ # TODO: We should also take into account the active modes of the other services (this is not trivial to do)
124
+ other_started_services = active_services.difference({service.name})
125
+ dependent_service_name = None
126
+ for other_started_service in other_started_services:
127
+ other_service = find_matching_service(other_started_service)
128
+ other_service_active_starting_modes = state.get_active_modes_for_service(
129
+ other_service.name, StateTables.STARTING_SERVICES
130
+ )
131
+ other_service_active_started_modes = state.get_active_modes_for_service(
132
+ other_service.name, StateTables.STARTED_SERVICES
133
+ )
134
+ other_service_active_modes = (
135
+ other_service_active_starting_modes
136
+ or other_service_active_started_modes
137
+ )
138
+ dependency_graph = construct_dependency_graph(
139
+ other_service, other_service_active_modes
140
+ )
141
+ # If the service we are trying to bring down is in the dependency graph of another service, we should not bring it down
142
+ if service.name in dependency_graph.graph:
143
+ dependent_service_name = other_started_service
144
+ break
145
+
146
+ # If no other service depends on the service we are trying to bring down, we can bring it down
147
+ if dependent_service_name is None:
148
+ try:
149
+ _down(service, remote_dependencies, list(mode_dependencies), status)
150
+ except DockerComposeError as dce:
151
+ capture_exception(dce)
152
+ status.failure(f"Failed to stop {service.name}: {dce.stderr}")
153
+ exit(1)
154
+ else:
155
+ status.warning(
156
+ f"Leaving {service.name} running because it is being used by {dependent_service_name}"
157
+ )
158
+
108
159
  # TODO: We should factor in healthchecks here before marking service as not running
109
- state = State()
110
- state.remove_started_service(service.name)
160
+ state.remove_service_entry(service.name, StateTables.STARTING_SERVICES)
161
+ state.remove_service_entry(service.name, StateTables.STARTED_SERVICES)
162
+ if dependent_service_name is None:
163
+ console.success(f"{service.name} stopped")
111
164
 
112
165
 
113
166
  def _bring_down_dependency(
@@ -141,7 +194,7 @@ def _down(
141
194
  service=service,
142
195
  remote_dependencies=list(remote_dependencies),
143
196
  current_env=current_env,
144
- command="down",
197
+ command="stop",
145
198
  options=[],
146
199
  service_config_file_path=service_config_file_path,
147
200
  mode_dependencies=mode_dependencies,
@@ -7,6 +7,7 @@ from argparse import Namespace
7
7
  from sentry_sdk import capture_exception
8
8
 
9
9
  from devservices.exceptions import ConfigError
10
+ from devservices.exceptions import ConfigNotFoundError
10
11
  from devservices.exceptions import ServiceNotFoundError
11
12
  from devservices.utils.console import Console
12
13
  from devservices.utils.services import find_matching_service
@@ -32,6 +33,12 @@ def list_dependencies(args: Namespace) -> None:
32
33
 
33
34
  try:
34
35
  service = find_matching_service(service_name)
36
+ except ConfigNotFoundError as e:
37
+ capture_exception(e)
38
+ console.failure(
39
+ f"{str(e)}. Please specify a service (i.e. `devservices list-dependencies sentry`) or run the command from a directory with a devservices configuration."
40
+ )
41
+ exit(1)
35
42
  except ConfigError as e:
36
43
  capture_exception(e)
37
44
  console.failure(str(e))
@@ -8,6 +8,7 @@ from devservices.utils.console import Console
8
8
  from devservices.utils.devenv import get_coderoot
9
9
  from devservices.utils.services import get_local_services
10
10
  from devservices.utils.state import State
11
+ from devservices.utils.state import StateTables
11
12
 
12
13
 
13
14
  def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
@@ -30,7 +31,9 @@ def list_services(args: Namespace) -> None:
30
31
  coderoot = get_coderoot()
31
32
  services = get_local_services(coderoot)
32
33
  state = State()
33
- running_services = state.get_started_services()
34
+ starting_services = set(state.get_service_entries(StateTables.STARTING_SERVICES))
35
+ started_services = set(state.get_service_entries(StateTables.STARTED_SERVICES))
36
+ running_services = starting_services.union(started_services)
34
37
 
35
38
  if not services:
36
39
  console.warning("No services found")
@@ -46,8 +49,18 @@ def list_services(args: Namespace) -> None:
46
49
  console.info("Running services:")
47
50
 
48
51
  for service in services_to_show:
49
- status = "running" if service.name in running_services else "stopped"
50
- active_modes = state.get_active_modes_for_service(service.name)
52
+ status = "stopped"
53
+ if service.name in starting_services:
54
+ status = "starting"
55
+ elif service.name in started_services:
56
+ status = "started"
57
+ active_starting_modes = state.get_active_modes_for_service(
58
+ service.name, StateTables.STARTING_SERVICES
59
+ )
60
+ active_started_modes = state.get_active_modes_for_service(
61
+ service.name, StateTables.STARTED_SERVICES
62
+ )
63
+ active_modes = active_starting_modes or active_started_modes
51
64
  console.info(f"- {service.name}")
52
65
  console.info(f" modes: {active_modes}")
53
66
  console.info(f" status: {status}")
@@ -16,6 +16,7 @@ from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
16
16
  from devservices.constants import DEVSERVICES_DIR_NAME
17
17
  from devservices.constants import MAX_LOG_LINES
18
18
  from devservices.exceptions import ConfigError
19
+ from devservices.exceptions import ConfigNotFoundError
19
20
  from devservices.exceptions import DependencyError
20
21
  from devservices.exceptions import DockerComposeError
21
22
  from devservices.exceptions import ServiceNotFoundError
@@ -27,6 +28,7 @@ from devservices.utils.docker_compose import run_cmd
27
28
  from devservices.utils.services import find_matching_service
28
29
  from devservices.utils.services import Service
29
30
  from devservices.utils.state import State
31
+ from devservices.utils.state import StateTables
30
32
 
31
33
 
32
34
  def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
@@ -46,6 +48,12 @@ def logs(args: Namespace) -> None:
46
48
  service_name = args.service_name
47
49
  try:
48
50
  service = find_matching_service(service_name)
51
+ except ConfigNotFoundError as e:
52
+ capture_exception(e)
53
+ console.failure(
54
+ f"{str(e)}. Please specify a service (i.e. `devservices logs sentry`) or run the command from a directory with a devservices configuration."
55
+ )
56
+ exit(1)
49
57
  except ConfigError as e:
50
58
  capture_exception(e)
51
59
  console.failure(str(e))
@@ -60,7 +68,9 @@ def logs(args: Namespace) -> None:
60
68
  mode_dependencies = modes[mode_to_use]
61
69
 
62
70
  state = State()
63
- running_services = state.get_started_services()
71
+ starting_services = set(state.get_service_entries(StateTables.STARTING_SERVICES))
72
+ started_services = set(state.get_service_entries(StateTables.STARTED_SERVICES))
73
+ running_services = starting_services.union(started_services)
64
74
  if service.name not in running_services:
65
75
  console.warning(f"Service {service.name} is not running")
66
76
  return
@@ -69,7 +79,9 @@ def logs(args: Namespace) -> None:
69
79
  remote_dependencies = install_and_verify_dependencies(service)
70
80
  except DependencyError as de:
71
81
  capture_exception(de)
72
- console.failure(str(de))
82
+ console.failure(
83
+ f"{str(de)}. If this error persists, try running `devservices purge`"
84
+ )
73
85
  exit(1)
74
86
  try:
75
87
  logs_output = _logs(service, remote_dependencies, mode_dependencies)
@@ -17,6 +17,7 @@ from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
17
17
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
18
18
  from devservices.constants import DEVSERVICES_DIR_NAME
19
19
  from devservices.exceptions import ConfigError
20
+ from devservices.exceptions import ConfigNotFoundError
20
21
  from devservices.exceptions import DependencyError
21
22
  from devservices.exceptions import DockerComposeError
22
23
  from devservices.exceptions import ServiceNotFoundError
@@ -86,6 +87,12 @@ def status(args: Namespace) -> None:
86
87
  service_name = args.service_name
87
88
  try:
88
89
  service = find_matching_service(service_name)
90
+ except ConfigNotFoundError as e:
91
+ capture_exception(e)
92
+ console.failure(
93
+ f"{str(e)}. Please specify a service (i.e. `devservices status sentry`) or run the command from a directory with a devservices configuration."
94
+ )
95
+ exit(1)
89
96
  except ConfigError as e:
90
97
  capture_exception(e)
91
98
  console.failure(str(e))
@@ -103,7 +110,9 @@ def status(args: Namespace) -> None:
103
110
  remote_dependencies = install_and_verify_dependencies(service)
104
111
  except DependencyError as de:
105
112
  capture_exception(de)
106
- console.failure(str(de))
113
+ console.failure(
114
+ f"{str(de)}. If this error persists, try running `devservices purge`"
115
+ )
107
116
  exit(1)
108
117
  try:
109
118
  status_json_results = _status(service, remote_dependencies, mode_dependencies)
@@ -15,6 +15,7 @@ from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
15
15
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
16
16
  from devservices.constants import DEVSERVICES_DIR_NAME
17
17
  from devservices.exceptions import ConfigError
18
+ from devservices.exceptions import ConfigNotFoundError
18
19
  from devservices.exceptions import ContainerHealthcheckFailedError
19
20
  from devservices.exceptions import DependencyError
20
21
  from devservices.exceptions import DockerComposeError
@@ -33,6 +34,7 @@ from devservices.utils.docker_compose import run_cmd
33
34
  from devservices.utils.services import find_matching_service
34
35
  from devservices.utils.services import Service
35
36
  from devservices.utils.state import State
37
+ from devservices.utils.state import StateTables
36
38
 
37
39
 
38
40
  def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
@@ -60,6 +62,12 @@ def up(args: Namespace) -> None:
60
62
  service_name = args.service_name
61
63
  try:
62
64
  service = find_matching_service(service_name)
65
+ except ConfigNotFoundError as e:
66
+ capture_exception(e)
67
+ console.failure(
68
+ f"{str(e)}. Please specify a service (i.e. `devservices up sentry`) or run the command from a directory with a devservices configuration."
69
+ )
70
+ exit(1)
63
71
  except ConfigError as e:
64
72
  capture_exception(e)
65
73
  console.failure(str(e))
@@ -71,6 +79,8 @@ def up(args: Namespace) -> None:
71
79
  modes = service.config.modes
72
80
  mode = args.mode
73
81
 
82
+ state = State()
83
+
74
84
  with Status(
75
85
  lambda: console.warning(f"Starting '{service.name}' in mode: '{mode}'"),
76
86
  lambda: console.success(f"{service.name} started"),
@@ -82,7 +92,9 @@ def up(args: Namespace) -> None:
82
92
  )
83
93
  except DependencyError as de:
84
94
  capture_exception(de)
85
- status.failure(str(de))
95
+ status.failure(
96
+ f"{str(de)}. If this error persists, try running `devservices purge`"
97
+ )
86
98
  exit(1)
87
99
  except ModeDoesNotExistError as mde:
88
100
  status.failure(str(mde))
@@ -92,6 +104,8 @@ def up(args: Namespace) -> None:
92
104
  except subprocess.CalledProcessError:
93
105
  # Network already exists, ignore the error
94
106
  pass
107
+ # Add the service to the starting services table
108
+ state.update_service_entry(service.name, mode, StateTables.STARTING_SERVICES)
95
109
  try:
96
110
  mode_dependencies = modes[mode]
97
111
  _up(service, [mode], remote_dependencies, mode_dependencies, status)
@@ -100,8 +114,8 @@ def up(args: Namespace) -> None:
100
114
  status.failure(f"Failed to start {service.name}: {dce.stderr}")
101
115
  exit(1)
102
116
  # TODO: We should factor in healthchecks here before marking service as running
103
- state = State()
104
- state.update_started_service(service.name, mode)
117
+ state.remove_service_entry(service.name, StateTables.STARTING_SERVICES)
118
+ state.update_service_entry(service.name, mode, StateTables.STARTED_SERVICES)
105
119
 
106
120
 
107
121
  def _bring_up_dependency(
@@ -67,7 +67,9 @@ class ServiceConfig:
67
67
  def load_service_config_from_file(repo_path: str) -> ServiceConfig:
68
68
  config_path = os.path.join(repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME)
69
69
  if not os.path.exists(config_path):
70
- raise ConfigNotFoundError(f"Config file not found in directory: {config_path}")
70
+ raise ConfigNotFoundError(
71
+ f"No devservices configuration found in {config_path}"
72
+ )
71
73
  with open(config_path, "r", encoding="utf-8") as stream:
72
74
  try:
73
75
  config = yaml.safe_load(stream)
@@ -70,27 +70,33 @@ class DockerError(Exception):
70
70
  class DockerComposeError(DockerError):
71
71
  """Base class for Docker Compose related errors."""
72
72
 
73
- pass
73
+ def __str__(self) -> str:
74
+ return f"DockerComposeError: {self.command} returned {self.returncode} error: {self.stderr}"
74
75
 
75
76
 
76
77
  class ModeDoesNotExistError(Exception):
77
78
  """Raised when a mode does not exist."""
78
79
 
79
- def __init__(self, service_name: str, mode: str):
80
+ def __init__(self, service_name: str, mode: str, available_modes: list[str]):
80
81
  self.service_name = service_name
81
82
  self.mode = mode
83
+ self.available_modes = available_modes
82
84
 
83
85
  def __str__(self) -> str:
84
- return f"ModeDoesNotExistError: Mode '{self.mode}' does not exist for service '{self.service_name}'"
86
+ # All valid services should have at least one mode, so we don't check for an empty list
87
+ return f"ModeDoesNotExistError: Mode '{self.mode}' does not exist for service '{self.service_name}'.\nAvailable modes: {', '.join(self.available_modes)}"
85
88
 
86
89
 
87
90
  class DependencyError(Exception):
88
91
  """Base class for dependency-related errors."""
89
92
 
90
- def __init__(self, repo_name: str, repo_link: str, branch: str):
93
+ def __init__(
94
+ self, repo_name: str, repo_link: str, branch: str, stderr: str | None = None
95
+ ):
91
96
  self.repo_name = repo_name
92
97
  self.repo_link = repo_link
93
98
  self.branch = branch
99
+ self.stderr = stderr
94
100
 
95
101
  def __str__(self) -> str:
96
102
  return f"DependencyError: {self.repo_name} ({self.repo_link}) on {self.branch}"
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import os
3
4
  import sys
4
5
  import threading
5
6
  import time
@@ -97,6 +98,8 @@ class Status:
97
98
  self.on_success()
98
99
 
99
100
  def _loading_animation(self) -> None:
101
+ if os.environ.get("CI", default="false") == "true":
102
+ return
100
103
  idx = 0
101
104
  while not self._stop_loading.is_set():
102
105
  sys.stdout.write("\r" + ANIMATION_FRAMES[idx % len(ANIMATION_FRAMES)] + " ")