devservices 1.0.7__tar.gz → 1.0.9__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 (57) hide show
  1. {devservices-1.0.7 → devservices-1.0.9}/PKG-INFO +4 -2
  2. {devservices-1.0.7 → devservices-1.0.9}/README.md +1 -1
  3. {devservices-1.0.7 → devservices-1.0.9}/devservices/commands/down.py +41 -8
  4. {devservices-1.0.7 → devservices-1.0.9}/devservices/commands/list_dependencies.py +7 -0
  5. {devservices-1.0.7 → devservices-1.0.9}/devservices/commands/logs.py +7 -0
  6. {devservices-1.0.7 → devservices-1.0.9}/devservices/commands/status.py +25 -8
  7. {devservices-1.0.7 → devservices-1.0.9}/devservices/commands/up.py +9 -3
  8. {devservices-1.0.7 → devservices-1.0.9}/devservices/configs/service_config.py +4 -2
  9. {devservices-1.0.7 → devservices-1.0.9}/devservices/constants.py +1 -1
  10. {devservices-1.0.7 → devservices-1.0.9}/devservices/main.py +0 -9
  11. {devservices-1.0.7 → devservices-1.0.9}/devservices/utils/console.py +3 -0
  12. {devservices-1.0.7 → devservices-1.0.9}/devservices/utils/dependencies.py +40 -7
  13. {devservices-1.0.7 → devservices-1.0.9}/devservices/utils/docker.py +1 -1
  14. {devservices-1.0.7 → devservices-1.0.9}/devservices/utils/docker_compose.py +28 -18
  15. {devservices-1.0.7 → devservices-1.0.9}/devservices/utils/file_lock.py +1 -1
  16. {devservices-1.0.7 → devservices-1.0.9}/devservices/utils/services.py +8 -1
  17. {devservices-1.0.7 → devservices-1.0.9}/devservices.egg-info/PKG-INFO +4 -2
  18. {devservices-1.0.7 → devservices-1.0.9}/devservices.egg-info/requires.txt +2 -0
  19. {devservices-1.0.7 → devservices-1.0.9}/pyproject.toml +3 -1
  20. devservices-1.0.9/tests/commands/test_down.py +861 -0
  21. {devservices-1.0.7 → devservices-1.0.9}/tests/commands/test_list_dependencies.py +21 -0
  22. {devservices-1.0.7 → devservices-1.0.9}/tests/commands/test_logs.py +22 -1
  23. {devservices-1.0.7 → devservices-1.0.9}/tests/commands/test_status.py +96 -1
  24. {devservices-1.0.7 → devservices-1.0.9}/tests/commands/test_up.py +171 -15
  25. {devservices-1.0.7 → devservices-1.0.9}/tests/configs/test_service_config.py +1 -1
  26. {devservices-1.0.7 → devservices-1.0.9}/tests/utils/test_docker_compose.py +3 -9
  27. {devservices-1.0.7 → devservices-1.0.9}/tests/utils/test_services.py +65 -2
  28. devservices-1.0.7/tests/commands/test_down.py +0 -265
  29. {devservices-1.0.7 → devservices-1.0.9}/LICENSE.md +0 -0
  30. {devservices-1.0.7 → devservices-1.0.9}/devservices/__init__.py +0 -0
  31. {devservices-1.0.7 → devservices-1.0.9}/devservices/commands/__init__.py +0 -0
  32. {devservices-1.0.7 → devservices-1.0.9}/devservices/commands/list_services.py +0 -0
  33. {devservices-1.0.7 → devservices-1.0.9}/devservices/commands/purge.py +0 -0
  34. {devservices-1.0.7 → devservices-1.0.9}/devservices/commands/update.py +0 -0
  35. {devservices-1.0.7 → devservices-1.0.9}/devservices/exceptions.py +0 -0
  36. {devservices-1.0.7 → devservices-1.0.9}/devservices/utils/__init__.py +0 -0
  37. {devservices-1.0.7 → devservices-1.0.9}/devservices/utils/check_for_update.py +0 -0
  38. {devservices-1.0.7 → devservices-1.0.9}/devservices/utils/devenv.py +0 -0
  39. {devservices-1.0.7 → devservices-1.0.9}/devservices/utils/install_binary.py +0 -0
  40. {devservices-1.0.7 → devservices-1.0.9}/devservices/utils/state.py +0 -0
  41. {devservices-1.0.7 → devservices-1.0.9}/devservices.egg-info/SOURCES.txt +0 -0
  42. {devservices-1.0.7 → devservices-1.0.9}/devservices.egg-info/dependency_links.txt +0 -0
  43. {devservices-1.0.7 → devservices-1.0.9}/devservices.egg-info/entry_points.txt +0 -0
  44. {devservices-1.0.7 → devservices-1.0.9}/devservices.egg-info/top_level.txt +0 -0
  45. {devservices-1.0.7 → devservices-1.0.9}/setup.cfg +0 -0
  46. {devservices-1.0.7 → devservices-1.0.9}/testing/__init__.py +0 -0
  47. {devservices-1.0.7 → devservices-1.0.9}/testing/utils.py +0 -0
  48. {devservices-1.0.7 → devservices-1.0.9}/tests/__init__.py +0 -0
  49. {devservices-1.0.7 → devservices-1.0.9}/tests/commands/test_list_services.py +0 -0
  50. {devservices-1.0.7 → devservices-1.0.9}/tests/commands/test_purge.py +0 -0
  51. {devservices-1.0.7 → devservices-1.0.9}/tests/commands/test_update.py +0 -0
  52. {devservices-1.0.7 → devservices-1.0.9}/tests/conftest.py +0 -0
  53. {devservices-1.0.7 → devservices-1.0.9}/tests/utils/test_check_for_update.py +0 -0
  54. {devservices-1.0.7 → devservices-1.0.9}/tests/utils/test_dependencies.py +0 -0
  55. {devservices-1.0.7 → devservices-1.0.9}/tests/utils/test_docker.py +0 -0
  56. {devservices-1.0.7 → devservices-1.0.9}/tests/utils/test_install_binary.py +0 -0
  57. {devservices-1.0.7 → devservices-1.0.9}/tests/utils/test_state.py +0 -0
@@ -1,10 +1,12 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: devservices
3
- Version: 1.0.7
3
+ Version: 1.0.9
4
4
  Requires-Python: >=3.10
5
5
  License-File: LICENSE.md
6
6
  Requires-Dist: pyyaml
7
7
  Requires-Dist: sentry-devenv
8
+ Requires-Dist: sentry-sdk
9
+ Requires-Dist: packaging
8
10
  Provides-Extra: dev
9
11
  Requires-Dist: black; extra == "dev"
10
12
  Requires-Dist: mypy; extra == "dev"
@@ -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.7
14
+ devservices==1.0.9
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
@@ -56,6 +58,12 @@ def down(args: Namespace) -> None:
56
58
  service_name = args.service_name
57
59
  try:
58
60
  service = find_matching_service(service_name)
61
+ except ConfigNotFoundError as e:
62
+ capture_exception(e)
63
+ console.failure(
64
+ f"{str(e)}. Please specify a service (i.e. `devservices down sentry`) or run the command from a directory with a devservices configuration."
65
+ )
66
+ exit(1)
59
67
  except ConfigError as e:
60
68
  capture_exception(e)
61
69
  console.failure(str(e))
@@ -80,7 +88,6 @@ def down(args: Namespace) -> None:
80
88
 
81
89
  with Status(
82
90
  lambda: console.warning(f"Stopping {service.name}"),
83
- lambda: console.success(f"{service.name} stopped"),
84
91
  ) as status:
85
92
  try:
86
93
  remote_dependencies = install_and_verify_dependencies(
@@ -98,16 +105,42 @@ def down(args: Namespace) -> None:
98
105
  capture_exception(de)
99
106
  status.failure(str(de))
100
107
  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}")
106
- exit(1)
108
+
109
+ # Check if any service depends on the service we are trying to bring down
110
+ # TODO: We should also take into account the active modes of the other services (this is not trivial to do)
111
+ other_started_services = set(started_services).difference({service.name})
112
+ dependent_service_name = None
113
+ for other_started_service in other_started_services:
114
+ other_service = find_matching_service(other_started_service)
115
+ other_service_active_modes = state.get_active_modes_for_service(
116
+ other_service.name
117
+ )
118
+ dependency_graph = construct_dependency_graph(
119
+ other_service, other_service_active_modes
120
+ )
121
+ # If the service we are trying to bring down is in the dependency graph of another service, we should not bring it down
122
+ if service.name in dependency_graph.graph:
123
+ dependent_service_name = other_started_service
124
+ break
125
+
126
+ # If no other service depends on the service we are trying to bring down, we can bring it down
127
+ if dependent_service_name is None:
128
+ try:
129
+ _down(service, remote_dependencies, list(mode_dependencies), status)
130
+ except DockerComposeError as dce:
131
+ capture_exception(dce)
132
+ status.failure(f"Failed to stop {service.name}: {dce.stderr}")
133
+ exit(1)
134
+ else:
135
+ status.warning(
136
+ f"Leaving {service.name} running because it is being used by {dependent_service_name}"
137
+ )
107
138
 
108
139
  # TODO: We should factor in healthchecks here before marking service as not running
109
140
  state = State()
110
141
  state.remove_started_service(service.name)
142
+ if dependent_service_name is None:
143
+ console.success(f"{service.name} stopped")
111
144
 
112
145
 
113
146
  def _bring_down_dependency(
@@ -141,7 +174,7 @@ def _down(
141
174
  service=service,
142
175
  remote_dependencies=list(remote_dependencies),
143
176
  current_env=current_env,
144
- command="down",
177
+ command="stop",
145
178
  options=[],
146
179
  service_config_file_path=service_config_file_path,
147
180
  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))
@@ -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
@@ -46,6 +47,12 @@ def logs(args: Namespace) -> None:
46
47
  service_name = args.service_name
47
48
  try:
48
49
  service = find_matching_service(service_name)
50
+ except ConfigNotFoundError as e:
51
+ capture_exception(e)
52
+ console.failure(
53
+ f"{str(e)}. Please specify a service (i.e. `devservices logs sentry`) or run the command from a directory with a devservices configuration."
54
+ )
55
+ exit(1)
49
56
  except ConfigError as e:
50
57
  capture_exception(e)
51
58
  console.failure(str(e))
@@ -7,6 +7,7 @@ import subprocess
7
7
  from argparse import _SubParsersAction
8
8
  from argparse import ArgumentParser
9
9
  from argparse import Namespace
10
+ from collections import namedtuple
10
11
 
11
12
  from sentry_sdk import capture_exception
12
13
 
@@ -16,6 +17,7 @@ from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
16
17
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
17
18
  from devservices.constants import DEVSERVICES_DIR_NAME
18
19
  from devservices.exceptions import ConfigError
20
+ from devservices.exceptions import ConfigNotFoundError
19
21
  from devservices.exceptions import DependencyError
20
22
  from devservices.exceptions import DockerComposeError
21
23
  from devservices.exceptions import ServiceNotFoundError
@@ -30,6 +32,9 @@ from devservices.utils.services import Service
30
32
  LINE_LENGTH = 40
31
33
 
32
34
 
35
+ ServiceStatus = namedtuple("ServiceStatus", ["name", "formatted_output"])
36
+
37
+
33
38
  def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
34
39
  parser = subparsers.add_parser("status", help="View status of a service")
35
40
  parser.add_argument(
@@ -41,12 +46,12 @@ def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
41
46
  parser.set_defaults(func=status)
42
47
 
43
48
 
44
- def format_status_output(status_json: str) -> str:
49
+ def format_status_output(service_status_json: str) -> list[ServiceStatus]:
45
50
  # Docker compose ps is line delimited json, so this constructs this into an array we can use
46
- service_statuses = status_json.split("\n")[:-1]
47
- output = []
48
- output.append("-" * LINE_LENGTH)
51
+ service_statuses = service_status_json.split("\n")[:-1]
52
+ outputs = []
49
53
  for service_status in service_statuses:
54
+ output = []
50
55
  service = json.loads(service_status)
51
56
  name = service["Service"]
52
57
  state = service["State"]
@@ -71,8 +76,9 @@ def format_status_output(status_json: str) -> str:
71
76
  output.append("No ports exposed")
72
77
 
73
78
  output.append("") # Empty line for readability
79
+ outputs.append(ServiceStatus(name=name, formatted_output="\n".join(output)))
74
80
 
75
- return "\n".join(output)
81
+ return outputs
76
82
 
77
83
 
78
84
  def status(args: Namespace) -> None:
@@ -81,6 +87,12 @@ def status(args: Namespace) -> None:
81
87
  service_name = args.service_name
82
88
  try:
83
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)
84
96
  except ConfigError as e:
85
97
  capture_exception(e)
86
98
  console.failure(str(e))
@@ -115,10 +127,15 @@ def status(args: Namespace) -> None:
115
127
  console.warning(f"{service.name} is not running")
116
128
  return
117
129
  output = f"Service: {service.name}\n\n"
130
+ output += "=" * LINE_LENGTH + "\n"
131
+ formatted_status_outputs = []
118
132
  for status_json in status_json_results:
119
- output += format_status_output(status_json.stdout)
120
- output += "=" * LINE_LENGTH
121
- console.info(output + "\n")
133
+ formatted_status_outputs.extend(format_status_output(status_json.stdout))
134
+ formatted_status_outputs.sort(key=lambda x: x.name)
135
+ for formatted_status_output in formatted_status_outputs:
136
+ output += formatted_status_output[1]
137
+ output += "-" * LINE_LENGTH + "\n"
138
+ console.info(output)
122
139
 
123
140
 
124
141
  def _status(
@@ -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
@@ -60,6 +61,12 @@ def up(args: Namespace) -> None:
60
61
  service_name = args.service_name
61
62
  try:
62
63
  service = find_matching_service(service_name)
64
+ except ConfigNotFoundError as e:
65
+ capture_exception(e)
66
+ console.failure(
67
+ f"{str(e)}. Please specify a service (i.e. `devservices up sentry`) or run the command from a directory with a devservices configuration."
68
+ )
69
+ exit(1)
63
70
  except ConfigError as e:
64
71
  capture_exception(e)
65
72
  console.failure(str(e))
@@ -107,7 +114,6 @@ def up(args: Namespace) -> None:
107
114
  def _bring_up_dependency(
108
115
  cmd: DockerComposeCommand, current_env: dict[str, str], status: Status
109
116
  ) -> subprocess.CompletedProcess[str]:
110
- # TODO: Get rid of these constants, we need a smarter way to determine the containers being brought up
111
117
  for dependency in cmd.services:
112
118
  status.info(f"Starting {dependency}")
113
119
  return run_cmd(cmd.full_command, current_env)
@@ -132,7 +138,6 @@ def _up(
132
138
  current_env[
133
139
  DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
134
140
  ] = relative_local_dependency_directory
135
- options = ["-d", "--pull", "always"]
136
141
  dependency_graph = construct_dependency_graph(service, modes=modes)
137
142
  starting_order = dependency_graph.get_starting_order()
138
143
  sorted_remote_dependencies = sorted(
@@ -143,7 +148,7 @@ def _up(
143
148
  remote_dependencies=sorted_remote_dependencies,
144
149
  current_env=current_env,
145
150
  command="up",
146
- options=options,
151
+ options=["-d", "--pull", "always"],
147
152
  service_config_file_path=service_config_file_path,
148
153
  mode_dependencies=mode_dependencies,
149
154
  )
@@ -180,4 +185,5 @@ def _create_devservices_network() -> None:
180
185
  ["docker", "network", "create", "devservices"],
181
186
  stdout=subprocess.DEVNULL,
182
187
  stderr=subprocess.DEVNULL,
188
+ check=True,
183
189
  )
@@ -67,8 +67,10 @@ 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}")
71
- with open(config_path, "r") as stream:
70
+ raise ConfigNotFoundError(
71
+ f"No devservices configuration found in {config_path}"
72
+ )
73
+ with open(config_path, "r", encoding="utf-8") as stream:
72
74
  try:
73
75
  config = yaml.safe_load(stream)
74
76
  except yaml.YAMLError as yml_error:
@@ -37,5 +37,5 @@ DEVSERVICES_LATEST_VERSION_CACHE_FILE = os.path.join(
37
37
  DEVSERVICES_CACHE_DIR, "latest_version.txt"
38
38
  )
39
39
  DEVSERVICES_LATEST_VERSION_CACHE_TTL = timedelta(minutes=15)
40
- HEALTHCHECK_TIMEOUT = 30
40
+ HEALTHCHECK_TIMEOUT = 45
41
41
  HEALTHCHECK_INTERVAL = 5
@@ -26,7 +26,6 @@ from devservices.commands import update
26
26
  from devservices.constants import LOGGER_NAME
27
27
  from devservices.exceptions import DockerComposeInstallationError
28
28
  from devservices.exceptions import DockerDaemonNotRunningError
29
- from devservices.utils.check_for_update import check_for_update
30
29
  from devservices.utils.console import Console
31
30
  from devservices.utils.docker_compose import check_docker_compose_version
32
31
 
@@ -102,14 +101,6 @@ def main() -> None:
102
101
  else:
103
102
  parser.print_help()
104
103
 
105
- if args.command != "update" and os.environ.get("CI") != "true":
106
- newest_version = check_for_update()
107
- if newest_version != current_version:
108
- console.warning(
109
- f"WARNING: A new version of devservices is available: {newest_version}"
110
- )
111
- console.warning('To update, run: "devservices update"')
112
-
113
104
 
114
105
  if __name__ == "__main__":
115
106
  main()
@@ -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)] + " ")
@@ -5,6 +5,7 @@ import os
5
5
  import shutil
6
6
  import subprocess
7
7
  import tempfile
8
+ import time
8
9
  from collections import deque
9
10
  from concurrent.futures import as_completed
10
11
  from concurrent.futures import ThreadPoolExecutor
@@ -264,15 +265,28 @@ def get_non_shared_remote_dependencies(
264
265
  # We don't care about the remote dependencies of the service we are stopping
265
266
  started_services.remove(service_to_stop.name)
266
267
  other_running_remote_dependencies: set[InstalledRemoteDependency] = set()
267
- for service_name in started_services:
268
- service = find_matching_service(service_name)
268
+ base_running_service_names: set[str] = set()
269
+ for started_service_name in started_services:
270
+ started_service = find_matching_service(started_service_name)
271
+ for dependency_name in service_to_stop.config.dependencies.keys():
272
+ if dependency_name == started_service.config.service_name:
273
+ base_running_service_names.add(started_service_name)
274
+ installed_remote_dependencies = get_installed_remote_dependencies(
275
+ list(started_service.config.dependencies.values())
276
+ )
269
277
  # TODO: There is an edge case here where there is a shared remote dependency with different modes
270
278
  other_running_remote_dependencies = other_running_remote_dependencies.union(
271
- get_installed_remote_dependencies(
272
- list(service.config.dependencies.values())
273
- )
279
+ installed_remote_dependencies
274
280
  )
275
- return remote_dependencies.difference(other_running_remote_dependencies)
281
+ non_shared_remote_dependencies = remote_dependencies.difference(
282
+ other_running_remote_dependencies
283
+ )
284
+ non_shared_remote_dependencies = {
285
+ dependency
286
+ for dependency in non_shared_remote_dependencies
287
+ if dependency.service_name not in base_running_service_names
288
+ }
289
+ return non_shared_remote_dependencies
276
290
 
277
291
 
278
292
  def get_installed_remote_dependencies(
@@ -448,7 +462,7 @@ def _update_dependency(
448
462
  ) from e
449
463
 
450
464
  try:
451
- _run_command(
465
+ _run_command_with_retries(
452
466
  ["git", "fetch", "origin", dependency.branch, "--filter=blob:none"],
453
467
  cwd=dependency_repo_dir,
454
468
  )
@@ -574,6 +588,25 @@ def _run_command(
574
588
  subprocess.run(cmd, cwd=cwd, check=True, stdout=stdout, stderr=subprocess.DEVNULL)
575
589
 
576
590
 
591
+ def _run_command_with_retries(
592
+ cmd: list[str],
593
+ cwd: str,
594
+ stdout: int | TextIO | None = subprocess.DEVNULL,
595
+ retries: int = 3,
596
+ backoff: int = 2,
597
+ ) -> None:
598
+ for i in range(retries):
599
+ try:
600
+ _run_command(cmd, cwd=cwd, stdout=stdout)
601
+ break
602
+ except subprocess.CalledProcessError as e:
603
+ logger = logging.getLogger(LOGGER_NAME)
604
+ logger.exception("Attempt %s of %s failed: %s", i + 1, retries, e)
605
+ if i == retries - 1:
606
+ raise e
607
+ time.sleep(backoff**i)
608
+
609
+
577
610
  def _try_set_git_config_context(
578
611
  git_config_manager: GitConfigManager,
579
612
  ) -> None:
@@ -66,7 +66,7 @@ def wait_for_healthy(container_name: str, status: Status) -> None:
66
66
 
67
67
  if result == "healthy":
68
68
  return
69
- elif result == "unknown":
69
+ if result == "unknown":
70
70
  status.warning(
71
71
  f"WARNING: Container {container_name} does not have a healthcheck"
72
72
  )
@@ -5,7 +5,6 @@ import os
5
5
  import platform
6
6
  import re
7
7
  import subprocess
8
- from collections.abc import Callable
9
8
  from typing import cast
10
9
  from typing import NamedTuple
11
10
 
@@ -170,7 +169,7 @@ def check_docker_compose_version() -> None:
170
169
 
171
170
 
172
171
  # TODO: Consider removing this in favor of in house logic for determining non-remote services
173
- def _get_non_remote_services(
172
+ def get_non_remote_services(
174
173
  service_config_path: str, current_env: dict[str, str]
175
174
  ) -> set[str]:
176
175
  config_command = [
@@ -195,19 +194,14 @@ def _get_non_remote_services(
195
194
  return set(config_services.splitlines())
196
195
 
197
196
 
198
- def get_docker_compose_commands_to_run(
199
- service: Service,
200
- remote_dependencies: list[InstalledRemoteDependency],
201
- current_env: dict[str, str],
197
+ def create_docker_compose_command(
198
+ name: str,
199
+ config_path: str,
200
+ services_to_use: set[str],
202
201
  command: str,
203
202
  options: list[str],
204
- service_config_file_path: str,
205
- mode_dependencies: list[str],
206
- ) -> list[DockerComposeCommand]:
207
- docker_compose_commands = []
208
- create_docker_compose_command: Callable[
209
- [str, str, set[str]], DockerComposeCommand
210
- ] = lambda name, config_path, services_to_use: DockerComposeCommand(
203
+ ) -> DockerComposeCommand:
204
+ return DockerComposeCommand(
211
205
  full_command=[
212
206
  "docker",
213
207
  "compose",
@@ -223,13 +217,25 @@ def get_docker_compose_commands_to_run(
223
217
  config_path=config_path,
224
218
  services=sorted(list(services_to_use)),
225
219
  )
220
+
221
+
222
+ def get_docker_compose_commands_to_run(
223
+ service: Service,
224
+ remote_dependencies: list[InstalledRemoteDependency],
225
+ current_env: dict[str, str],
226
+ command: str,
227
+ options: list[str],
228
+ service_config_file_path: str,
229
+ mode_dependencies: list[str],
230
+ ) -> list[DockerComposeCommand]:
231
+ docker_compose_commands = []
226
232
  for dependency in remote_dependencies:
227
233
  # TODO: Consider passing in service config in InstalledRemoteDependency instead of loading it here
228
234
  dependency_service_config = load_service_config_from_file(dependency.repo_path)
229
235
  dependency_config_path = os.path.join(
230
236
  dependency.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
231
237
  )
232
- non_remote_services = _get_non_remote_services(
238
+ non_remote_services = get_non_remote_services(
233
239
  dependency_config_path, current_env
234
240
  )
235
241
  services_to_use = non_remote_services.intersection(
@@ -240,18 +246,22 @@ def get_docker_compose_commands_to_run(
240
246
  dependency_service_config.service_name,
241
247
  dependency_config_path,
242
248
  services_to_use,
249
+ command,
250
+ options,
243
251
  )
244
252
  )
245
253
 
246
254
  # Add docker compose command for the top level service
247
- non_remote_services = _get_non_remote_services(
248
- service_config_file_path, current_env
249
- )
255
+ non_remote_services = get_non_remote_services(service_config_file_path, current_env)
250
256
  services_to_use = non_remote_services.intersection(set(mode_dependencies))
251
257
  if len(services_to_use) > 0:
252
258
  docker_compose_commands.append(
253
259
  create_docker_compose_command(
254
- service.name, service_config_file_path, services_to_use
260
+ service.name,
261
+ service_config_file_path,
262
+ services_to_use,
263
+ command,
264
+ options,
255
265
  )
256
266
  )
257
267
  return docker_compose_commands
@@ -7,7 +7,7 @@ from collections.abc import Generator
7
7
 
8
8
  @contextlib.contextmanager
9
9
  def lock(path: str) -> Generator[None, None, None]:
10
- with open(path, mode="a+") as f:
10
+ with open(path, mode="a+", encoding="utf-8") as f:
11
11
  with _locked(f.fileno()):
12
12
  yield
13
13
 
@@ -65,4 +65,11 @@ def find_matching_service(service_name: str | None = None) -> Service:
65
65
  for service in services:
66
66
  if service.name.lower() == service_name.lower():
67
67
  return service
68
- raise ServiceNotFoundError(f'Service "{service_name}" not found')
68
+ unique_service_names = sorted(set(service.name for service in services))
69
+ error_message = f"Service '{service_name}' not found."
70
+ if len(unique_service_names) > 0:
71
+ service_bullet_points = "\n".join(
72
+ [f"- {service_name}" for service_name in unique_service_names]
73
+ )
74
+ error_message += "\nSupported services:\n" + service_bullet_points
75
+ raise ServiceNotFoundError(error_message)
@@ -1,10 +1,12 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: devservices
3
- Version: 1.0.7
3
+ Version: 1.0.9
4
4
  Requires-Python: >=3.10
5
5
  License-File: LICENSE.md
6
6
  Requires-Dist: pyyaml
7
7
  Requires-Dist: sentry-devenv
8
+ Requires-Dist: sentry-sdk
9
+ Requires-Dist: packaging
8
10
  Provides-Extra: dev
9
11
  Requires-Dist: black; extra == "dev"
10
12
  Requires-Dist: mypy; extra == "dev"
@@ -1,5 +1,7 @@
1
1
  pyyaml
2
2
  sentry-devenv
3
+ sentry-sdk
4
+ packaging
3
5
 
4
6
  [dev]
5
7
  black
@@ -4,12 +4,14 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devservices"
7
- version = "1.0.7"
7
+ version = "1.0.9"
8
8
  # 3.10 is just for internal pypi compat
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
11
11
  "pyyaml",
12
12
  "sentry-devenv",
13
+ "sentry-sdk",
14
+ "packaging",
13
15
  ]
14
16
 
15
17
  [project.optional-dependencies]