devservices 1.0.9__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 (58) hide show
  1. {devservices-1.0.9 → devservices-1.0.10}/PKG-INFO +1 -1
  2. {devservices-1.0.9 → devservices-1.0.10}/README.md +1 -1
  3. {devservices-1.0.9 → devservices-1.0.10}/devservices/commands/down.py +30 -10
  4. {devservices-1.0.9 → devservices-1.0.10}/devservices/commands/list_services.py +16 -3
  5. {devservices-1.0.9 → devservices-1.0.10}/devservices/commands/logs.py +7 -2
  6. {devservices-1.0.9 → devservices-1.0.10}/devservices/commands/status.py +3 -1
  7. {devservices-1.0.9 → devservices-1.0.10}/devservices/commands/up.py +10 -3
  8. {devservices-1.0.9 → devservices-1.0.10}/devservices/exceptions.py +10 -4
  9. {devservices-1.0.9 → devservices-1.0.10}/devservices/utils/dependencies.py +83 -23
  10. {devservices-1.0.9 → devservices-1.0.10}/devservices/utils/docker_compose.py +1 -1
  11. {devservices-1.0.9 → devservices-1.0.10}/devservices/utils/state.py +48 -22
  12. {devservices-1.0.9 → devservices-1.0.10}/devservices.egg-info/PKG-INFO +1 -1
  13. {devservices-1.0.9 → devservices-1.0.10}/pyproject.toml +1 -1
  14. {devservices-1.0.9 → devservices-1.0.10}/tests/commands/test_down.py +192 -83
  15. devservices-1.0.10/tests/commands/test_list_services.py +176 -0
  16. {devservices-1.0.9 → devservices-1.0.10}/tests/commands/test_logs.py +27 -8
  17. {devservices-1.0.9 → devservices-1.0.10}/tests/commands/test_purge.py +64 -27
  18. {devservices-1.0.9 → devservices-1.0.10}/tests/commands/test_up.py +289 -294
  19. {devservices-1.0.9 → devservices-1.0.10}/tests/utils/test_dependencies.py +619 -6
  20. devservices-1.0.10/tests/utils/test_state.py +84 -0
  21. devservices-1.0.9/tests/commands/test_list_services.py +0 -89
  22. devservices-1.0.9/tests/utils/test_state.py +0 -54
  23. {devservices-1.0.9 → devservices-1.0.10}/LICENSE.md +0 -0
  24. {devservices-1.0.9 → devservices-1.0.10}/devservices/__init__.py +0 -0
  25. {devservices-1.0.9 → devservices-1.0.10}/devservices/commands/__init__.py +0 -0
  26. {devservices-1.0.9 → devservices-1.0.10}/devservices/commands/list_dependencies.py +0 -0
  27. {devservices-1.0.9 → devservices-1.0.10}/devservices/commands/purge.py +0 -0
  28. {devservices-1.0.9 → devservices-1.0.10}/devservices/commands/update.py +0 -0
  29. {devservices-1.0.9 → devservices-1.0.10}/devservices/configs/service_config.py +0 -0
  30. {devservices-1.0.9 → devservices-1.0.10}/devservices/constants.py +0 -0
  31. {devservices-1.0.9 → devservices-1.0.10}/devservices/main.py +0 -0
  32. {devservices-1.0.9 → devservices-1.0.10}/devservices/utils/__init__.py +0 -0
  33. {devservices-1.0.9 → devservices-1.0.10}/devservices/utils/check_for_update.py +0 -0
  34. {devservices-1.0.9 → devservices-1.0.10}/devservices/utils/console.py +0 -0
  35. {devservices-1.0.9 → devservices-1.0.10}/devservices/utils/devenv.py +0 -0
  36. {devservices-1.0.9 → devservices-1.0.10}/devservices/utils/docker.py +0 -0
  37. {devservices-1.0.9 → devservices-1.0.10}/devservices/utils/file_lock.py +0 -0
  38. {devservices-1.0.9 → devservices-1.0.10}/devservices/utils/install_binary.py +0 -0
  39. {devservices-1.0.9 → devservices-1.0.10}/devservices/utils/services.py +0 -0
  40. {devservices-1.0.9 → devservices-1.0.10}/devservices.egg-info/SOURCES.txt +0 -0
  41. {devservices-1.0.9 → devservices-1.0.10}/devservices.egg-info/dependency_links.txt +0 -0
  42. {devservices-1.0.9 → devservices-1.0.10}/devservices.egg-info/entry_points.txt +0 -0
  43. {devservices-1.0.9 → devservices-1.0.10}/devservices.egg-info/requires.txt +0 -0
  44. {devservices-1.0.9 → devservices-1.0.10}/devservices.egg-info/top_level.txt +0 -0
  45. {devservices-1.0.9 → devservices-1.0.10}/setup.cfg +0 -0
  46. {devservices-1.0.9 → devservices-1.0.10}/testing/__init__.py +0 -0
  47. {devservices-1.0.9 → devservices-1.0.10}/testing/utils.py +0 -0
  48. {devservices-1.0.9 → devservices-1.0.10}/tests/__init__.py +0 -0
  49. {devservices-1.0.9 → devservices-1.0.10}/tests/commands/test_list_dependencies.py +0 -0
  50. {devservices-1.0.9 → devservices-1.0.10}/tests/commands/test_status.py +0 -0
  51. {devservices-1.0.9 → devservices-1.0.10}/tests/commands/test_update.py +0 -0
  52. {devservices-1.0.9 → devservices-1.0.10}/tests/configs/test_service_config.py +0 -0
  53. {devservices-1.0.9 → devservices-1.0.10}/tests/conftest.py +0 -0
  54. {devservices-1.0.9 → devservices-1.0.10}/tests/utils/test_check_for_update.py +0 -0
  55. {devservices-1.0.9 → devservices-1.0.10}/tests/utils/test_docker.py +0 -0
  56. {devservices-1.0.9 → devservices-1.0.10}/tests/utils/test_docker_compose.py +0 -0
  57. {devservices-1.0.9 → devservices-1.0.10}/tests/utils/test_install_binary.py +0 -0
  58. {devservices-1.0.9 → devservices-1.0.10}/tests/utils/test_services.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: devservices
3
- Version: 1.0.9
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.9
14
+ devservices==1.0.10
15
15
  ```
16
16
 
17
17
 
@@ -31,6 +31,7 @@ from devservices.utils.docker_compose import run_cmd
31
31
  from devservices.utils.services import find_matching_service
32
32
  from devservices.utils.services import Service
33
33
  from devservices.utils.state import State
34
+ from devservices.utils.state import StateTables
34
35
 
35
36
 
36
37
  def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
@@ -75,12 +76,20 @@ def down(args: Namespace) -> None:
75
76
  modes = service.config.modes
76
77
 
77
78
  state = State()
78
- started_services = state.get_started_services()
79
- 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:
80
83
  console.warning(f"{service.name} is not running")
81
84
  exit(0)
82
85
 
83
- 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
84
93
  mode_dependencies = set()
85
94
  for active_mode in active_modes:
86
95
  active_mode_dependencies = modes.get(active_mode, [])
@@ -95,7 +104,9 @@ def down(args: Namespace) -> None:
95
104
  )
96
105
  except DependencyError as de:
97
106
  capture_exception(de)
98
- status.failure(str(de))
107
+ status.failure(
108
+ f"{str(de)}. If this error persists, try running `devservices purge`"
109
+ )
99
110
  exit(1)
100
111
  try:
101
112
  remote_dependencies = get_non_shared_remote_dependencies(
@@ -103,17 +114,26 @@ def down(args: Namespace) -> None:
103
114
  )
104
115
  except DependencyError as de:
105
116
  capture_exception(de)
106
- status.failure(str(de))
117
+ status.failure(
118
+ f"{str(de)}. If this error persists, try running `devservices purge`"
119
+ )
107
120
  exit(1)
108
121
 
109
122
  # Check if any service depends on the service we are trying to bring down
110
123
  # 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})
124
+ other_started_services = active_services.difference({service.name})
112
125
  dependent_service_name = None
113
126
  for other_started_service in other_started_services:
114
127
  other_service = find_matching_service(other_started_service)
115
- other_service_active_modes = state.get_active_modes_for_service(
116
- other_service.name
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
117
137
  )
118
138
  dependency_graph = construct_dependency_graph(
119
139
  other_service, other_service_active_modes
@@ -137,8 +157,8 @@ def down(args: Namespace) -> None:
137
157
  )
138
158
 
139
159
  # TODO: We should factor in healthchecks here before marking service as not running
140
- state = State()
141
- 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)
142
162
  if dependent_service_name is None:
143
163
  console.success(f"{service.name} stopped")
144
164
 
@@ -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}")
@@ -28,6 +28,7 @@ from devservices.utils.docker_compose import run_cmd
28
28
  from devservices.utils.services import find_matching_service
29
29
  from devservices.utils.services import Service
30
30
  from devservices.utils.state import State
31
+ from devservices.utils.state import StateTables
31
32
 
32
33
 
33
34
  def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
@@ -67,7 +68,9 @@ def logs(args: Namespace) -> None:
67
68
  mode_dependencies = modes[mode_to_use]
68
69
 
69
70
  state = State()
70
- 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)
71
74
  if service.name not in running_services:
72
75
  console.warning(f"Service {service.name} is not running")
73
76
  return
@@ -76,7 +79,9 @@ def logs(args: Namespace) -> None:
76
79
  remote_dependencies = install_and_verify_dependencies(service)
77
80
  except DependencyError as de:
78
81
  capture_exception(de)
79
- console.failure(str(de))
82
+ console.failure(
83
+ f"{str(de)}. If this error persists, try running `devservices purge`"
84
+ )
80
85
  exit(1)
81
86
  try:
82
87
  logs_output = _logs(service, remote_dependencies, mode_dependencies)
@@ -110,7 +110,9 @@ def status(args: Namespace) -> None:
110
110
  remote_dependencies = install_and_verify_dependencies(service)
111
111
  except DependencyError as de:
112
112
  capture_exception(de)
113
- console.failure(str(de))
113
+ console.failure(
114
+ f"{str(de)}. If this error persists, try running `devservices purge`"
115
+ )
114
116
  exit(1)
115
117
  try:
116
118
  status_json_results = _status(service, remote_dependencies, mode_dependencies)
@@ -34,6 +34,7 @@ from devservices.utils.docker_compose import run_cmd
34
34
  from devservices.utils.services import find_matching_service
35
35
  from devservices.utils.services import Service
36
36
  from devservices.utils.state import State
37
+ from devservices.utils.state import StateTables
37
38
 
38
39
 
39
40
  def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
@@ -78,6 +79,8 @@ def up(args: Namespace) -> None:
78
79
  modes = service.config.modes
79
80
  mode = args.mode
80
81
 
82
+ state = State()
83
+
81
84
  with Status(
82
85
  lambda: console.warning(f"Starting '{service.name}' in mode: '{mode}'"),
83
86
  lambda: console.success(f"{service.name} started"),
@@ -89,7 +92,9 @@ def up(args: Namespace) -> None:
89
92
  )
90
93
  except DependencyError as de:
91
94
  capture_exception(de)
92
- status.failure(str(de))
95
+ status.failure(
96
+ f"{str(de)}. If this error persists, try running `devservices purge`"
97
+ )
93
98
  exit(1)
94
99
  except ModeDoesNotExistError as mde:
95
100
  status.failure(str(mde))
@@ -99,6 +104,8 @@ def up(args: Namespace) -> None:
99
104
  except subprocess.CalledProcessError:
100
105
  # Network already exists, ignore the error
101
106
  pass
107
+ # Add the service to the starting services table
108
+ state.update_service_entry(service.name, mode, StateTables.STARTING_SERVICES)
102
109
  try:
103
110
  mode_dependencies = modes[mode]
104
111
  _up(service, [mode], remote_dependencies, mode_dependencies, status)
@@ -107,8 +114,8 @@ def up(args: Namespace) -> None:
107
114
  status.failure(f"Failed to start {service.name}: {dce.stderr}")
108
115
  exit(1)
109
116
  # TODO: We should factor in healthchecks here before marking service as running
110
- state = State()
111
- 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)
112
119
 
113
120
 
114
121
  def _bring_up_dependency(
@@ -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}"
@@ -13,6 +13,7 @@ from dataclasses import dataclass
13
13
  from typing import TextIO
14
14
  from typing import TypeGuard
15
15
 
16
+ from sentry_sdk import capture_message
16
17
  from sentry_sdk import set_context
17
18
 
18
19
  from devservices.configs.service_config import Dependency
@@ -38,6 +39,7 @@ from devservices.utils.file_lock import lock
38
39
  from devservices.utils.services import find_matching_service
39
40
  from devservices.utils.services import Service
40
41
  from devservices.utils.state import State
42
+ from devservices.utils.state import StateTables
41
43
 
42
44
  RELEVANT_GIT_CONFIG_KEYS = [
43
45
  "init.defaultbranch",
@@ -208,7 +210,11 @@ def install_and_verify_dependencies(
208
210
  mode_dependencies = set()
209
211
  for mode in modes:
210
212
  if mode not in service.config.modes:
211
- raise ModeDoesNotExistError(service_name=service.name, mode=mode)
213
+ raise ModeDoesNotExistError(
214
+ service_name=service.name,
215
+ mode=mode,
216
+ available_modes=list(service.config.modes.keys()),
217
+ )
212
218
  mode_dependencies.update(service.config.modes[mode])
213
219
  matching_dependencies = [
214
220
  dependency
@@ -261,12 +267,14 @@ def get_non_shared_remote_dependencies(
261
267
  service_to_stop: Service, remote_dependencies: set[InstalledRemoteDependency]
262
268
  ) -> set[InstalledRemoteDependency]:
263
269
  state = State()
264
- started_services = state.get_started_services()
270
+ starting_services = set(state.get_service_entries(StateTables.STARTING_SERVICES))
271
+ started_services = set(state.get_service_entries(StateTables.STARTED_SERVICES))
272
+ active_services = starting_services.union(started_services)
265
273
  # We don't care about the remote dependencies of the service we are stopping
266
- started_services.remove(service_to_stop.name)
274
+ active_services.remove(service_to_stop.name)
267
275
  other_running_remote_dependencies: set[InstalledRemoteDependency] = set()
268
276
  base_running_service_names: set[str] = set()
269
- for started_service_name in started_services:
277
+ for started_service_name in active_services:
270
278
  started_service = find_matching_service(started_service_name)
271
279
  for dependency_name in service_to_stop.config.dependencies.keys():
272
280
  if dependency_name == started_service.config.service_name:
@@ -408,6 +416,7 @@ def install_dependency(dependency: RemoteConfig) -> set[InstalledRemoteDependenc
408
416
  raise ModeDoesNotExistError(
409
417
  service_name=installed_config.service_name,
410
418
  mode=dependency.mode,
419
+ available_modes=list(installed_config.modes.keys()),
411
420
  )
412
421
 
413
422
  active_nested_dependencies = [
@@ -473,27 +482,49 @@ def _update_dependency(
473
482
  repo_name=dependency.repo_name,
474
483
  repo_link=dependency.repo_link,
475
484
  branch=dependency.branch,
485
+ stderr=e.stderr,
476
486
  ) from e
477
487
 
478
488
  # Check if the local repo is up-to-date
479
- local_commit = subprocess.check_output(
480
- ["git", "rev-parse", "HEAD"],
481
- cwd=dependency_repo_dir,
482
- stderr=subprocess.PIPE,
483
- ).strip()
484
-
485
- remote_commit = subprocess.check_output(
486
- ["git", "rev-parse", "FETCH_HEAD"],
487
- cwd=dependency_repo_dir,
488
- stderr=subprocess.PIPE,
489
- ).strip()
489
+ try:
490
+ local_commit = _rev_parse(dependency_repo_dir, "HEAD")
491
+ except subprocess.CalledProcessError as e:
492
+ raise DependencyError(
493
+ repo_name=dependency.repo_name,
494
+ repo_link=dependency.repo_link,
495
+ branch=dependency.branch,
496
+ stderr=e.stderr,
497
+ ) from e
498
+
499
+ try:
500
+ remote_commit = _rev_parse(dependency_repo_dir, "FETCH_HEAD")
501
+ except subprocess.CalledProcessError as e:
502
+ raise DependencyError(
503
+ repo_name=dependency.repo_name,
504
+ repo_link=dependency.repo_link,
505
+ branch=dependency.branch,
506
+ stderr=e.stderr,
507
+ ) from e
490
508
 
491
509
  if local_commit == remote_commit:
492
510
  # Already up-to-date, don't pull anything
511
+ logger = logging.getLogger(LOGGER_NAME)
512
+ logger.debug(
513
+ "Dependency %s is already up-to-date, not pulling anything",
514
+ dependency.repo_name,
515
+ )
493
516
  return
494
517
 
495
518
  # If it's not up-to-date, checkout the latest changes (forcibly)
496
- _run_command(["git", "checkout", "-f", "FETCH_HEAD"], cwd=dependency_repo_dir)
519
+ try:
520
+ _run_command(["git", "checkout", "-f", "FETCH_HEAD"], cwd=dependency_repo_dir)
521
+ except subprocess.CalledProcessError as e:
522
+ raise DependencyError(
523
+ repo_name=dependency.repo_name,
524
+ repo_link=dependency.repo_link,
525
+ branch=dependency.branch,
526
+ stderr=e.stderr,
527
+ ) from e
497
528
 
498
529
 
499
530
  def _checkout_dependency(
@@ -518,6 +549,7 @@ def _checkout_dependency(
518
549
  repo_name=dependency.repo_name,
519
550
  repo_link=dependency.repo_link,
520
551
  branch=dependency.branch,
552
+ stderr=e.stderr,
521
553
  ) from e
522
554
 
523
555
  # Setup config for partial clone and sparse checkout
@@ -535,10 +567,18 @@ def _checkout_dependency(
535
567
  branch=dependency.branch,
536
568
  ) from e
537
569
 
538
- _run_command(
539
- ["git", "checkout", dependency.branch],
540
- cwd=temp_dir,
541
- )
570
+ try:
571
+ _run_command(
572
+ ["git", "checkout", dependency.branch],
573
+ cwd=temp_dir,
574
+ )
575
+ except subprocess.CalledProcessError as e:
576
+ raise DependencyError(
577
+ repo_name=dependency.repo_name,
578
+ repo_link=dependency.repo_link,
579
+ branch=dependency.branch,
580
+ stderr=e.stderr,
581
+ ) from e
542
582
 
543
583
  # Clean up the existing directory if it exists
544
584
  if os.path.exists(dependency_repo_dir):
@@ -580,12 +620,26 @@ def _has_remote_config(remote_config: RemoteConfig | None) -> TypeGuard[RemoteCo
580
620
  return remote_config is not None
581
621
 
582
622
 
623
+ def _rev_parse(repo_dir: str, ref: str) -> str:
624
+ logger = logging.getLogger(LOGGER_NAME)
625
+ logger.debug("Parsing revision for %s (%s)", ref, repo_dir)
626
+ rev = (
627
+ subprocess.check_output(
628
+ ["git", "rev-parse", ref], cwd=repo_dir, stderr=subprocess.PIPE
629
+ )
630
+ .strip()
631
+ .decode()
632
+ )
633
+ logger.debug("Parsed revision %s for %s (%s)", rev, ref, repo_dir)
634
+ return rev
635
+
636
+
583
637
  def _run_command(
584
638
  cmd: list[str], cwd: str, stdout: int | TextIO | None = subprocess.DEVNULL
585
639
  ) -> None:
586
640
  logger = logging.getLogger(LOGGER_NAME)
587
- logger.debug(f"Running command: {' '.join(cmd)} in {cwd}")
588
- subprocess.run(cmd, cwd=cwd, check=True, stdout=stdout, stderr=subprocess.DEVNULL)
641
+ logger.debug("Running command: %s in %s", " ".join(cmd), cwd)
642
+ subprocess.run(cmd, cwd=cwd, check=True, stdout=stdout, stderr=subprocess.PIPE)
589
643
 
590
644
 
591
645
  def _run_command_with_retries(
@@ -601,7 +655,13 @@ def _run_command_with_retries(
601
655
  break
602
656
  except subprocess.CalledProcessError as e:
603
657
  logger = logging.getLogger(LOGGER_NAME)
604
- logger.exception("Attempt %s of %s failed: %s", i + 1, retries, e)
658
+ logger.debug(
659
+ "Attempt %s of %s for %s failed: %s", i + 1, retries, cmd, e.stderr
660
+ )
661
+ capture_message(
662
+ f"Attempt {i + 1} of {retries} for {cmd} failed: {e.stderr}",
663
+ level="warning",
664
+ )
605
665
  if i == retries - 1:
606
666
  raise e
607
667
  time.sleep(backoff**i)
@@ -270,7 +270,7 @@ def get_docker_compose_commands_to_run(
270
270
  def run_cmd(cmd: list[str], env: dict[str, str]) -> subprocess.CompletedProcess[str]:
271
271
  logger = logging.getLogger(LOGGER_NAME)
272
272
  try:
273
- logger.debug(f"Running command: {' '.join(cmd)}")
273
+ logger.debug("Running command: %s", " ".join(cmd))
274
274
  return subprocess.run(cmd, check=True, capture_output=True, text=True, env=env)
275
275
  except subprocess.CalledProcessError as e:
276
276
  raise DockerComposeError(
@@ -2,11 +2,17 @@ from __future__ import annotations
2
2
 
3
3
  import os
4
4
  import sqlite3
5
+ from enum import Enum
5
6
 
6
7
  from devservices.constants import DEVSERVICES_LOCAL_DIR
7
8
  from devservices.constants import STATE_DB_FILE
8
9
 
9
10
 
11
+ class StateTables(Enum):
12
+ STARTED_SERVICES = "started_services"
13
+ STARTING_SERVICES = "starting_services"
14
+
15
+
10
16
  class State:
11
17
  _instance: State | None = None
12
18
  state_db_file: str
@@ -24,9 +30,20 @@ class State:
24
30
 
25
31
  def initialize_database(self) -> None:
26
32
  cursor = self.conn.cursor()
33
+ # Formatted strings here and throughout the fileshould be extremely low risk given these are constants
27
34
  cursor.execute(
28
- """
29
- CREATE TABLE IF NOT EXISTS started_services (
35
+ f"""
36
+ CREATE TABLE IF NOT EXISTS {StateTables.STARTED_SERVICES.value} (
37
+ service_name TEXT PRIMARY KEY,
38
+ mode TEXT,
39
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
40
+ )
41
+ """
42
+ )
43
+
44
+ cursor.execute(
45
+ f"""
46
+ CREATE TABLE IF NOT EXISTS {StateTables.STARTING_SERVICES.value} (
30
47
  service_name TEXT PRIMARY KEY,
31
48
  mode TEXT,
32
49
  timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
@@ -35,52 +52,56 @@ class State:
35
52
  )
36
53
  self.conn.commit()
37
54
 
38
- def update_started_service(self, service_name: str, mode: str) -> None:
55
+ def update_service_entry(
56
+ self, service_name: str, mode: str, table: StateTables
57
+ ) -> None:
39
58
  cursor = self.conn.cursor()
40
- started_services = self.get_started_services()
41
- active_modes = self.get_active_modes_for_service(service_name)
42
- if service_name in started_services and mode in active_modes:
59
+ service_entries = self.get_service_entries(table)
60
+ active_modes = self.get_active_modes_for_service(service_name, table)
61
+ if service_name in service_entries and mode in active_modes:
43
62
  return
44
- if service_name in started_services:
63
+ if service_name in service_entries:
45
64
  cursor.execute(
46
- """
47
- UPDATE started_services SET mode = ? WHERE service_name = ?
65
+ f"""
66
+ UPDATE {table.value} SET mode = ? WHERE service_name = ?
48
67
  """,
49
68
  (",".join(active_modes + [mode]), service_name),
50
69
  )
51
70
  else:
52
71
  cursor.execute(
53
- """
54
- INSERT INTO started_services (service_name, mode) VALUES (?, ?)
72
+ f"""
73
+ INSERT INTO {table.value} (service_name, mode) VALUES (?, ?)
55
74
  """,
56
75
  (service_name, ",".join(active_modes + [mode])),
57
76
  )
58
77
  self.conn.commit()
59
78
 
60
- def remove_started_service(self, service_name: str) -> None:
79
+ def remove_service_entry(self, service_name: str, table: StateTables) -> None:
61
80
  cursor = self.conn.cursor()
62
81
  cursor.execute(
63
- """
64
- DELETE FROM started_services WHERE service_name = ?
82
+ f"""
83
+ DELETE FROM {table.value} WHERE service_name = ?
65
84
  """,
66
85
  (service_name,),
67
86
  )
68
87
  self.conn.commit()
69
88
 
70
- def get_started_services(self) -> list[str]:
89
+ def get_service_entries(self, table: StateTables) -> list[str]:
71
90
  cursor = self.conn.cursor()
72
91
  cursor.execute(
73
- """
74
- SELECT service_name FROM started_services
92
+ f"""
93
+ SELECT service_name FROM {table.value}
75
94
  """
76
95
  )
77
96
  return [row[0] for row in cursor.fetchall()]
78
97
 
79
- def get_active_modes_for_service(self, service_name: str) -> list[str]:
98
+ def get_active_modes_for_service(
99
+ self, service_name: str, table: StateTables
100
+ ) -> list[str]:
80
101
  cursor = self.conn.cursor()
81
102
  cursor.execute(
82
- """
83
- SELECT mode FROM started_services WHERE service_name = ?
103
+ f"""
104
+ SELECT mode FROM {table.value} WHERE service_name = ?
84
105
  """,
85
106
  (service_name,),
86
107
  )
@@ -92,8 +113,13 @@ class State:
92
113
  def clear_state(self) -> None:
93
114
  cursor = self.conn.cursor()
94
115
  cursor.execute(
95
- """
96
- DELETE FROM started_services
116
+ f"""
117
+ DELETE FROM {StateTables.STARTED_SERVICES.value}
118
+ """
119
+ )
120
+ cursor.execute(
121
+ f"""
122
+ DELETE FROM {StateTables.STARTING_SERVICES.value}
97
123
  """
98
124
  )
99
125
  self.conn.commit()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: devservices
3
- Version: 1.0.9
3
+ Version: 1.0.10
4
4
  Requires-Python: >=3.10
5
5
  License-File: LICENSE.md
6
6
  Requires-Dist: pyyaml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devservices"
7
- version = "1.0.9"
7
+ version = "1.0.10"
8
8
  # 3.10 is just for internal pypi compat
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [