devservices 1.2.2__tar.gz → 1.2.3__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 (69) hide show
  1. {devservices-1.2.2 → devservices-1.2.3}/PKG-INFO +1 -1
  2. {devservices-1.2.2 → devservices-1.2.3}/README.md +1 -1
  3. {devservices-1.2.2 → devservices-1.2.3}/devservices/commands/down.py +10 -0
  4. devservices-1.2.3/devservices/commands/purge.py +219 -0
  5. {devservices-1.2.2 → devservices-1.2.3}/devservices/commands/up.py +11 -0
  6. {devservices-1.2.2 → devservices-1.2.3}/devservices/main.py +6 -1
  7. {devservices-1.2.2 → devservices-1.2.3}/devservices/utils/dependencies.py +27 -0
  8. {devservices-1.2.2 → devservices-1.2.3}/devservices/utils/docker_compose.py +9 -0
  9. {devservices-1.2.2 → devservices-1.2.3}/devservices.egg-info/PKG-INFO +1 -1
  10. {devservices-1.2.2 → devservices-1.2.3}/pyproject.toml +1 -1
  11. {devservices-1.2.2 → devservices-1.2.3}/tests/commands/test_purge.py +144 -0
  12. devservices-1.2.2/devservices/commands/purge.py +0 -96
  13. {devservices-1.2.2 → devservices-1.2.3}/LICENSE.md +0 -0
  14. {devservices-1.2.2 → devservices-1.2.3}/devservices/__init__.py +0 -0
  15. {devservices-1.2.2 → devservices-1.2.3}/devservices/commands/__init__.py +0 -0
  16. {devservices-1.2.2 → devservices-1.2.3}/devservices/commands/foreground.py +0 -0
  17. {devservices-1.2.2 → devservices-1.2.3}/devservices/commands/list_dependencies.py +0 -0
  18. {devservices-1.2.2 → devservices-1.2.3}/devservices/commands/list_services.py +0 -0
  19. {devservices-1.2.2 → devservices-1.2.3}/devservices/commands/logs.py +0 -0
  20. {devservices-1.2.2 → devservices-1.2.3}/devservices/commands/reset.py +0 -0
  21. {devservices-1.2.2 → devservices-1.2.3}/devservices/commands/serve.py +0 -0
  22. {devservices-1.2.2 → devservices-1.2.3}/devservices/commands/status.py +0 -0
  23. {devservices-1.2.2 → devservices-1.2.3}/devservices/commands/toggle.py +0 -0
  24. {devservices-1.2.2 → devservices-1.2.3}/devservices/commands/update.py +0 -0
  25. {devservices-1.2.2 → devservices-1.2.3}/devservices/configs/service_config.py +0 -0
  26. {devservices-1.2.2 → devservices-1.2.3}/devservices/constants.py +0 -0
  27. {devservices-1.2.2 → devservices-1.2.3}/devservices/exceptions.py +0 -0
  28. {devservices-1.2.2 → devservices-1.2.3}/devservices/utils/__init__.py +0 -0
  29. {devservices-1.2.2 → devservices-1.2.3}/devservices/utils/check_for_update.py +0 -0
  30. {devservices-1.2.2 → devservices-1.2.3}/devservices/utils/console.py +0 -0
  31. {devservices-1.2.2 → devservices-1.2.3}/devservices/utils/devenv.py +0 -0
  32. {devservices-1.2.2 → devservices-1.2.3}/devservices/utils/docker.py +0 -0
  33. {devservices-1.2.2 → devservices-1.2.3}/devservices/utils/file_lock.py +0 -0
  34. {devservices-1.2.2 → devservices-1.2.3}/devservices/utils/git.py +0 -0
  35. {devservices-1.2.2 → devservices-1.2.3}/devservices/utils/install_binary.py +0 -0
  36. {devservices-1.2.2 → devservices-1.2.3}/devservices/utils/services.py +0 -0
  37. {devservices-1.2.2 → devservices-1.2.3}/devservices/utils/state.py +0 -0
  38. {devservices-1.2.2 → devservices-1.2.3}/devservices/utils/supervisor.py +0 -0
  39. {devservices-1.2.2 → devservices-1.2.3}/devservices.egg-info/SOURCES.txt +0 -0
  40. {devservices-1.2.2 → devservices-1.2.3}/devservices.egg-info/dependency_links.txt +0 -0
  41. {devservices-1.2.2 → devservices-1.2.3}/devservices.egg-info/entry_points.txt +0 -0
  42. {devservices-1.2.2 → devservices-1.2.3}/devservices.egg-info/requires.txt +0 -0
  43. {devservices-1.2.2 → devservices-1.2.3}/devservices.egg-info/top_level.txt +0 -0
  44. {devservices-1.2.2 → devservices-1.2.3}/setup.cfg +0 -0
  45. {devservices-1.2.2 → devservices-1.2.3}/testing/__init__.py +0 -0
  46. {devservices-1.2.2 → devservices-1.2.3}/testing/utils.py +0 -0
  47. {devservices-1.2.2 → devservices-1.2.3}/tests/__init__.py +0 -0
  48. {devservices-1.2.2 → devservices-1.2.3}/tests/commands/test_down.py +0 -0
  49. {devservices-1.2.2 → devservices-1.2.3}/tests/commands/test_foreground.py +0 -0
  50. {devservices-1.2.2 → devservices-1.2.3}/tests/commands/test_list_dependencies.py +0 -0
  51. {devservices-1.2.2 → devservices-1.2.3}/tests/commands/test_list_services.py +0 -0
  52. {devservices-1.2.2 → devservices-1.2.3}/tests/commands/test_logs.py +0 -0
  53. {devservices-1.2.2 → devservices-1.2.3}/tests/commands/test_reset.py +0 -0
  54. {devservices-1.2.2 → devservices-1.2.3}/tests/commands/test_serve.py +0 -0
  55. {devservices-1.2.2 → devservices-1.2.3}/tests/commands/test_status.py +0 -0
  56. {devservices-1.2.2 → devservices-1.2.3}/tests/commands/test_toggle.py +0 -0
  57. {devservices-1.2.2 → devservices-1.2.3}/tests/commands/test_up.py +0 -0
  58. {devservices-1.2.2 → devservices-1.2.3}/tests/commands/test_update.py +0 -0
  59. {devservices-1.2.2 → devservices-1.2.3}/tests/configs/test_service_config.py +0 -0
  60. {devservices-1.2.2 → devservices-1.2.3}/tests/conftest.py +0 -0
  61. {devservices-1.2.2 → devservices-1.2.3}/tests/utils/test_check_for_update.py +0 -0
  62. {devservices-1.2.2 → devservices-1.2.3}/tests/utils/test_dependencies.py +0 -0
  63. {devservices-1.2.2 → devservices-1.2.3}/tests/utils/test_docker.py +0 -0
  64. {devservices-1.2.2 → devservices-1.2.3}/tests/utils/test_docker_compose.py +0 -0
  65. {devservices-1.2.2 → devservices-1.2.3}/tests/utils/test_git.py +0 -0
  66. {devservices-1.2.2 → devservices-1.2.3}/tests/utils/test_install_binary.py +0 -0
  67. {devservices-1.2.2 → devservices-1.2.3}/tests/utils/test_services.py +0 -0
  68. {devservices-1.2.2 → devservices-1.2.3}/tests/utils/test_state.py +0 -0
  69. {devservices-1.2.2 → devservices-1.2.3}/tests/utils/test_supervisor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devservices
3
- Version: 1.2.2
3
+ Version: 1.2.3
4
4
  Requires-Python: >=3.11
5
5
  License-File: LICENSE.md
6
6
  Requires-Dist: pyyaml
@@ -33,7 +33,7 @@ NOTE: service-name is an optional parameter. If not provided, devservices will a
33
33
  The recommended way to install devservices is through a virtualenv in the requirements.txt. Once that is installed and a devservices config file is added, you should be able to run `devservices up` to begin local development.
34
34
 
35
35
  ```
36
- devservices==1.2.2
36
+ devservices==1.2.3
37
37
  ```
38
38
 
39
39
  ### 2. Add devservices config files
@@ -8,6 +8,7 @@ from argparse import ArgumentParser
8
8
  from argparse import Namespace
9
9
 
10
10
  from sentry_sdk import capture_exception
11
+ from sentry_sdk import logger as sentry_logger
11
12
 
12
13
  from devservices.constants import CONFIG_FILE_NAME
13
14
  from devservices.constants import DEPENDENCY_CONFIG_VERSION
@@ -116,6 +117,15 @@ def down(args: Namespace) -> None:
116
117
  == DependencyType.SUPERVISOR
117
118
  ]
118
119
 
120
+ sentry_logger.info(
121
+ "Stopping service",
122
+ extra={
123
+ "service_name": service.name,
124
+ "exclude_local": exclude_local,
125
+ "active_modes": list(active_modes),
126
+ },
127
+ )
128
+
119
129
  with Status(
120
130
  lambda: console.warning(f"Stopping {service.name}"),
121
131
  ) as status:
@@ -0,0 +1,219 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import shutil
5
+ from argparse import _SubParsersAction
6
+ from argparse import ArgumentParser
7
+ from argparse import Namespace
8
+
9
+ from devservices.configs.service_config import load_service_config_from_file
10
+ from devservices.constants import DEPENDENCY_CONFIG_VERSION
11
+ from devservices.constants import DEVSERVICES_CACHE_DIR
12
+ from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
13
+ from devservices.constants import DEVSERVICES_ORCHESTRATOR_LABEL
14
+ from devservices.constants import DOCKER_NETWORK_NAME
15
+ from devservices.exceptions import ConfigNotFoundError
16
+ from devservices.exceptions import ConfigParseError
17
+ from devservices.exceptions import ConfigValidationError
18
+ from devservices.exceptions import DockerDaemonNotRunningError
19
+ from devservices.exceptions import DockerError
20
+ from devservices.utils.console import Console
21
+ from devservices.utils.console import Status
22
+ from devservices.utils.docker import get_matching_containers
23
+ from devservices.utils.docker import get_matching_networks
24
+ from devservices.utils.docker import get_volumes_for_containers
25
+ from devservices.utils.docker import remove_docker_resources
26
+ from devservices.utils.docker import stop_containers
27
+ from devservices.utils.state import State
28
+ from devservices.utils.state import StateTables
29
+
30
+
31
+ def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
32
+ parser = subparsers.add_parser("purge", help="Purge the local devservices cache")
33
+ parser.add_argument(
34
+ "service_name",
35
+ nargs="?",
36
+ help="Service name to purge (optional, purges all if not specified)",
37
+ default=None,
38
+ )
39
+ parser.set_defaults(func=purge)
40
+
41
+
42
+ def _get_service_cache_paths(service_name: str) -> list[str]:
43
+ """Find cache directory paths for a given service name."""
44
+
45
+ cache_paths: list[str] = []
46
+ dependencies_cache_dir = os.path.join(
47
+ DEVSERVICES_DEPENDENCIES_CACHE_DIR, DEPENDENCY_CONFIG_VERSION
48
+ )
49
+
50
+ if not os.path.exists(dependencies_cache_dir):
51
+ return cache_paths
52
+
53
+ for repo_name in os.listdir(dependencies_cache_dir):
54
+ repo_path = os.path.join(dependencies_cache_dir, repo_name)
55
+ if not os.path.isdir(repo_path):
56
+ continue
57
+
58
+ try:
59
+ service_config = load_service_config_from_file(repo_path)
60
+ if service_config.service_name == service_name:
61
+ cache_paths.append(repo_path)
62
+ except (ConfigNotFoundError, ConfigParseError, ConfigValidationError):
63
+ # Skip invalid configs
64
+ continue
65
+
66
+ return cache_paths
67
+
68
+
69
+ def purge(args: Namespace) -> None:
70
+ """Purge the local devservices state and cache and remove all devservices containers and volumes."""
71
+ console = Console()
72
+ service_name = getattr(args, "service_name", None)
73
+
74
+ if service_name:
75
+ _purge_service(service_name, console)
76
+ else:
77
+ _purge_all(console)
78
+
79
+
80
+ def _purge_service(service_name: str, console: Console) -> None:
81
+ """Purge a specific service."""
82
+ state = State()
83
+
84
+ # Warn user about potential dependency issues
85
+ if not console.confirm(
86
+ f"WARNING: Purging {service_name} may introduce issues with the dependency tree.\n"
87
+ "Other services that depend on this service may stop working correctly.\n"
88
+ "Do you want to continue?"
89
+ ):
90
+ console.info("Purge cancelled.")
91
+ return
92
+
93
+ state.remove_service_entry(service_name, StateTables.SERVICE_RUNTIME)
94
+
95
+ try:
96
+ service_containers = get_matching_containers(
97
+ [
98
+ DEVSERVICES_ORCHESTRATOR_LABEL,
99
+ f"com.docker.compose.service={service_name}",
100
+ ]
101
+ )
102
+ except DockerDaemonNotRunningError as e:
103
+ console.warning(str(e))
104
+ service_containers = []
105
+ except DockerError as de:
106
+ console.failure(f"Failed to get containers for {service_name}: {de.stderr}")
107
+ exit(1)
108
+
109
+ if len(service_containers) == 0:
110
+ console.warning(f"No containers found for {service_name}")
111
+ else:
112
+ try:
113
+ service_volumes = get_volumes_for_containers(service_containers)
114
+ except DockerError as e:
115
+ console.failure(f"Failed to get volumes for {service_name}: {e.stderr}")
116
+ exit(1)
117
+
118
+ with Status(
119
+ lambda: console.warning(f"Stopping {service_name} containers"),
120
+ lambda: console.success(f"{service_name} containers have been stopped"),
121
+ ):
122
+ try:
123
+ stop_containers(service_containers, should_remove=True)
124
+ except DockerError as e:
125
+ console.failure(f"Failed to stop {service_name} containers: {e.stderr}")
126
+ exit(1)
127
+
128
+ console.warning(f"Removing {service_name} docker volumes")
129
+ if len(service_volumes) == 0:
130
+ console.success(f"No volumes found for {service_name}")
131
+ else:
132
+ try:
133
+ remove_docker_resources("volume", list(service_volumes))
134
+ console.success(f"{service_name} volumes removed")
135
+ except DockerError as e:
136
+ console.failure(f"Failed to remove {service_name} volumes: {e.stderr}")
137
+
138
+ cache_paths = _get_service_cache_paths(service_name)
139
+ if cache_paths:
140
+ console.warning(f"Removing cache for {service_name}")
141
+ for cache_path in cache_paths:
142
+ try:
143
+ shutil.rmtree(cache_path)
144
+ except PermissionError as e:
145
+ console.failure(f"Failed to remove cache at {cache_path}: {e}")
146
+ exit(1)
147
+ console.success(f"Cache for {service_name} has been removed")
148
+ else:
149
+ console.success(f"No cache found for {service_name}")
150
+
151
+ console.success(f"{service_name} has been purged")
152
+
153
+
154
+ def _purge_all(console: Console) -> None:
155
+ """Purge all devservices state, cache, containers, volumes, and networks."""
156
+ if os.path.exists(DEVSERVICES_CACHE_DIR):
157
+ try:
158
+ shutil.rmtree(DEVSERVICES_CACHE_DIR)
159
+ except PermissionError as e:
160
+ console.failure(f"Failed to purge cache: {e}")
161
+ exit(1)
162
+ state = State()
163
+ state.clear_state()
164
+
165
+ try:
166
+ devservices_containers = get_matching_containers(
167
+ [DEVSERVICES_ORCHESTRATOR_LABEL]
168
+ )
169
+ except DockerDaemonNotRunningError as e:
170
+ console.warning(str(e))
171
+ return
172
+ except DockerError as de:
173
+ console.failure(f"Failed to get devservices containers {de.stderr}")
174
+ exit(1)
175
+
176
+ try:
177
+ devservices_volumes = get_volumes_for_containers(devservices_containers)
178
+ except DockerError as e:
179
+ console.failure(f"Failed to get devservices volumes {e.stderr}")
180
+ exit(1)
181
+
182
+ with Status(
183
+ lambda: console.warning("Stopping all devservices containers"),
184
+ lambda: console.success("All devservices containers have been stopped"),
185
+ ):
186
+ try:
187
+ stop_containers(devservices_containers, should_remove=True)
188
+ except DockerError as e:
189
+ console.failure(f"Failed to stop devservices containers {e.stderr}")
190
+ exit(1)
191
+
192
+ console.warning("Removing any devservices docker volumes")
193
+ if len(devservices_volumes) == 0:
194
+ console.success("No devservices volumes found to remove")
195
+ else:
196
+ try:
197
+ remove_docker_resources("volume", list(devservices_volumes))
198
+ console.success("All devservices volumes removed")
199
+ except DockerError as e:
200
+ # We don't want to exit here since we still want to try to remove the networks
201
+ console.failure(f"Failed to remove devservices volumes {e.stderr}")
202
+
203
+ console.warning("Removing any devservices networks")
204
+ try:
205
+ devservices_networks = get_matching_networks(DOCKER_NETWORK_NAME)
206
+ except DockerError as e:
207
+ console.failure(f"Failed to get devservices networks {e.stderr}")
208
+ exit(1)
209
+ if len(devservices_networks) == 0:
210
+ console.success("No devservices networks found to remove")
211
+ else:
212
+ try:
213
+ remove_docker_resources("network", devservices_networks)
214
+ console.success("All devservices networks removed")
215
+ except DockerError as e:
216
+ console.failure(f"Failed to remove devservices networks {e.stderr}")
217
+ exit(1)
218
+
219
+ console.success("The local devservices cache and state has been purged")
@@ -8,6 +8,7 @@ from argparse import ArgumentParser
8
8
  from argparse import Namespace
9
9
 
10
10
  from sentry_sdk import capture_exception
11
+ from sentry_sdk import logger as sentry_logger
11
12
  from sentry_sdk import set_context
12
13
  from sentry_sdk import start_span
13
14
 
@@ -94,6 +95,16 @@ def up(args: Namespace, existing_status: Status | None = None) -> None:
94
95
  mode = args.mode
95
96
  exclude_local = getattr(args, "exclude_local", False)
96
97
 
98
+ sentry_logger.info(
99
+ "Starting service",
100
+ extra={
101
+ "service_name": service.name,
102
+ "mode": mode,
103
+ "exclude_local": exclude_local,
104
+ "available_modes": list(modes.keys()),
105
+ },
106
+ )
107
+
97
108
  state = State()
98
109
 
99
110
  with Status(
@@ -16,6 +16,7 @@ from sentry_sdk import set_tag
16
16
  from sentry_sdk import set_user
17
17
  from sentry_sdk import start_transaction
18
18
  from sentry_sdk.integrations.argv import ArgvIntegration
19
+ from sentry_sdk.integrations.logging import LoggingIntegration
19
20
  from sentry_sdk.types import Event
20
21
  from sentry_sdk.types import Hint
21
22
 
@@ -79,7 +80,11 @@ if not disable_sentry:
79
80
  dsn="https://56470da7302c16e83141f62f88e46449@o1.ingest.us.sentry.io/4507946704961536",
80
81
  traces_sample_rate=1.0,
81
82
  profiles_sample_rate=1.0,
82
- integrations=[ArgvIntegration()],
83
+ integrations=[
84
+ ArgvIntegration(),
85
+ LoggingIntegration(sentry_logs_level=logging.DEBUG),
86
+ ],
87
+ enable_logs=True,
83
88
  environment=sentry_environment,
84
89
  before_send=before_send_error,
85
90
  before_send_transaction=before_send_transaction,
@@ -14,6 +14,7 @@ from typing import TextIO
14
14
  from typing import TypeGuard
15
15
 
16
16
  from sentry_sdk import capture_message
17
+ from sentry_sdk import logger as sentry_logger
17
18
  from sentry_sdk import set_context
18
19
 
19
20
  from devservices.configs.service_config import Dependency
@@ -417,6 +418,16 @@ def install_dependency(dependency: RemoteConfig) -> set[InstalledRemoteDependenc
417
418
  dependency.repo_name,
418
419
  )
419
420
 
421
+ sentry_logger.info(
422
+ "Installing dependency",
423
+ extra={
424
+ "repo_name": dependency.repo_name,
425
+ "repo_link": dependency.repo_link,
426
+ "branch": dependency.branch,
427
+ "mode": dependency.mode,
428
+ },
429
+ )
430
+
420
431
  os.makedirs(DEVSERVICES_DEPENDENCIES_CACHE_DIR, exist_ok=True)
421
432
 
422
433
  # Ensure that only one process is installing a specific dependency at a time
@@ -498,6 +509,14 @@ def _update_dependency(
498
509
  dependency: RemoteConfig,
499
510
  dependency_repo_dir: str,
500
511
  ) -> None:
512
+ sentry_logger.info(
513
+ "Updating dependency",
514
+ extra={
515
+ "repo_name": dependency.repo_name,
516
+ "repo_link": dependency.repo_link,
517
+ "branch": dependency.branch,
518
+ },
519
+ )
501
520
  git_config_manager = GitConfigManager(
502
521
  dependency_repo_dir,
503
522
  DEPENDENCY_GIT_PARTIAL_CLONE_CONFIG_OPTIONS,
@@ -580,6 +599,14 @@ def _checkout_dependency(
580
599
  dependency: RemoteConfig,
581
600
  dependency_repo_dir: str,
582
601
  ) -> None:
602
+ sentry_logger.info(
603
+ "Checking out dependency",
604
+ extra={
605
+ "repo_name": dependency.repo_name,
606
+ "repo_link": dependency.repo_link,
607
+ "branch": dependency.branch,
608
+ },
609
+ )
583
610
  with tempfile.TemporaryDirectory() as temp_dir:
584
611
  try:
585
612
  _run_command(
@@ -12,6 +12,7 @@ from typing import cast
12
12
  from typing import NamedTuple
13
13
 
14
14
  from packaging import version
15
+ from sentry_sdk import logger as sentry_logger
15
16
 
16
17
  from devservices.configs.service_config import load_service_config_from_file
17
18
  from devservices.constants import CONFIG_FILE_NAME
@@ -306,6 +307,14 @@ def run_cmd(
306
307
  console = Console()
307
308
  cmd_pretty = shlex.join(cmd)
308
309
 
310
+ sentry_logger.info(
311
+ "Running docker compose command",
312
+ extra={
313
+ "command": cmd_pretty,
314
+ "max_retries": retries,
315
+ },
316
+ )
317
+
309
318
  proc = None
310
319
  retries += 1 # initial try
311
320
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devservices
3
- Version: 1.2.2
3
+ Version: 1.2.3
4
4
  Requires-Python: >=3.11
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.2.2"
7
+ version = "1.2.3"
8
8
  # 3.11 is just for internal pypi compat
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
@@ -552,3 +552,147 @@ def test_purge_with_cache_and_state_and_containers_with_networks_and_volumes(
552
552
  mock.call("network", ["abc", "def", "ghe"]),
553
553
  ]
554
554
  )
555
+
556
+
557
+ @mock.patch("devservices.commands.purge.Console.confirm")
558
+ @mock.patch("devservices.commands.purge.get_matching_containers")
559
+ @mock.patch("devservices.commands.purge.get_volumes_for_containers")
560
+ @mock.patch("devservices.commands.purge.stop_containers")
561
+ @mock.patch("devservices.commands.purge.remove_docker_resources")
562
+ @mock.patch("devservices.commands.purge._get_service_cache_paths")
563
+ def test_purge_specific_service(
564
+ mock_get_service_cache_paths: mock.Mock,
565
+ mock_remove_docker_resources: mock.Mock,
566
+ mock_stop_containers: mock.Mock,
567
+ mock_get_volumes_for_containers: mock.Mock,
568
+ mock_get_matching_containers: mock.Mock,
569
+ mock_console_confirm: mock.Mock,
570
+ tmp_path: Path,
571
+ ) -> None:
572
+ """Test that purging a specific service removes only that service's containers, volumes, and cache."""
573
+ mock_console_confirm.return_value = True
574
+ mock_get_matching_containers.return_value = [
575
+ "kafka-container-1",
576
+ "kafka-container-2",
577
+ ]
578
+ mock_get_volumes_for_containers.return_value = ["kafka-volume-1", "kafka-volume-2"]
579
+ cache_path = tmp_path / "dependencies" / "v1" / "kafka-repo"
580
+ cache_path.mkdir(parents=True, exist_ok=True)
581
+ mock_get_service_cache_paths.return_value = [str(cache_path)]
582
+
583
+ with (
584
+ mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")),
585
+ mock.patch(
586
+ "devservices.utils.docker.check_docker_daemon_running", return_value=None
587
+ ),
588
+ ):
589
+ state = State()
590
+ # Don't add kafka to STARTED_SERVICES - it should be stopped before purging
591
+ # Add redis to verify it's not affected by kafka purge
592
+ state.update_service_entry("redis", "default", StateTables.STARTED_SERVICES)
593
+
594
+ assert state.get_service_entries(StateTables.STARTED_SERVICES) == ["redis"]
595
+ assert cache_path.exists()
596
+
597
+ purge(Namespace(service_name="kafka"))
598
+
599
+ # redis should still be in state (unaffected)
600
+ assert state.get_service_entries(StateTables.STARTED_SERVICES) == ["redis"]
601
+ # Cache path should be removed
602
+ assert not cache_path.exists()
603
+
604
+ # Should filter containers by service name
605
+ mock_get_matching_containers.assert_called_once_with(
606
+ [
607
+ DEVSERVICES_ORCHESTRATOR_LABEL,
608
+ "com.docker.compose.service=kafka",
609
+ ]
610
+ )
611
+ mock_get_volumes_for_containers.assert_called_once_with(
612
+ ["kafka-container-1", "kafka-container-2"]
613
+ )
614
+ mock_stop_containers.assert_called_once_with(
615
+ ["kafka-container-1", "kafka-container-2"], should_remove=True
616
+ )
617
+ mock_remove_docker_resources.assert_called_once_with(
618
+ "volume", ["kafka-volume-1", "kafka-volume-2"]
619
+ )
620
+ mock_get_service_cache_paths.assert_called_once_with("kafka")
621
+
622
+
623
+ @mock.patch("devservices.commands.purge.Console.confirm")
624
+ @mock.patch("devservices.commands.purge.get_matching_containers")
625
+ @mock.patch("devservices.commands.purge._get_service_cache_paths")
626
+ def test_purge_specific_service_no_containers(
627
+ mock_get_service_cache_paths: mock.Mock,
628
+ mock_get_matching_containers: mock.Mock,
629
+ mock_console_confirm: mock.Mock,
630
+ capsys: pytest.CaptureFixture[str],
631
+ tmp_path: Path,
632
+ ) -> None:
633
+ """Test that purging a service with no containers or cache still removes it from state."""
634
+ mock_console_confirm.return_value = True
635
+ mock_get_matching_containers.return_value = []
636
+ mock_get_service_cache_paths.return_value = []
637
+
638
+ with (
639
+ mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")),
640
+ mock.patch(
641
+ "devservices.utils.docker.check_docker_daemon_running", return_value=None
642
+ ),
643
+ ):
644
+ state = State()
645
+ # Don't add kafka to STARTED_SERVICES - it should be stopped before purging
646
+
647
+ args = Namespace(service_name="kafka")
648
+ purge(args)
649
+
650
+ # State should remain empty (kafka was never added)
651
+ assert state.get_service_entries(StateTables.STARTED_SERVICES) == []
652
+
653
+ captured = capsys.readouterr()
654
+ assert "No containers found for kafka" in captured.out
655
+ assert "No cache found for kafka" in captured.out
656
+ assert "kafka has been purged" in captured.out
657
+
658
+
659
+ @mock.patch("devservices.commands.purge.Console.confirm")
660
+ @mock.patch("devservices.commands.purge.get_matching_containers")
661
+ @mock.patch("devservices.commands.purge._get_service_cache_paths")
662
+ def test_purge_specific_service_cancelled_by_user(
663
+ mock_get_service_cache_paths: mock.Mock,
664
+ mock_get_matching_containers: mock.Mock,
665
+ mock_console_confirm: mock.Mock,
666
+ capsys: pytest.CaptureFixture[str],
667
+ tmp_path: Path,
668
+ ) -> None:
669
+ """Test that purging a service can be cancelled by the user."""
670
+ mock_console_confirm.return_value = False
671
+ mock_get_matching_containers.return_value = []
672
+ mock_get_service_cache_paths.return_value = []
673
+
674
+ with (
675
+ mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")),
676
+ mock.patch(
677
+ "devservices.utils.docker.check_docker_daemon_running", return_value=None
678
+ ),
679
+ ):
680
+ state = State()
681
+ # Add kafka to state
682
+ state.update_service_entry("kafka", "default", StateTables.STARTED_SERVICES)
683
+
684
+ args = Namespace(service_name="kafka")
685
+ purge(args)
686
+
687
+ # Service should still be in state (purge was cancelled)
688
+ assert state.get_service_entries(StateTables.STARTED_SERVICES) == ["kafka"]
689
+
690
+ # Should have prompted user
691
+ mock_console_confirm.assert_called_once()
692
+
693
+ # Should not have attempted to get containers
694
+ mock_get_matching_containers.assert_not_called()
695
+ mock_get_service_cache_paths.assert_not_called()
696
+
697
+ captured = capsys.readouterr()
698
+ assert "Purge cancelled." in captured.out
@@ -1,96 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import os
4
- import shutil
5
- from argparse import _SubParsersAction
6
- from argparse import ArgumentParser
7
- from argparse import Namespace
8
-
9
- from devservices.constants import DEVSERVICES_CACHE_DIR
10
- from devservices.constants import DEVSERVICES_ORCHESTRATOR_LABEL
11
- from devservices.constants import DOCKER_NETWORK_NAME
12
- from devservices.exceptions import DockerDaemonNotRunningError
13
- from devservices.exceptions import DockerError
14
- from devservices.utils.console import Console
15
- from devservices.utils.console import Status
16
- from devservices.utils.docker import get_matching_containers
17
- from devservices.utils.docker import get_matching_networks
18
- from devservices.utils.docker import get_volumes_for_containers
19
- from devservices.utils.docker import remove_docker_resources
20
- from devservices.utils.docker import stop_containers
21
- from devservices.utils.state import State
22
-
23
-
24
- def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
25
- parser = subparsers.add_parser("purge", help="Purge the local devservices cache")
26
- parser.set_defaults(func=purge)
27
-
28
-
29
- def purge(_args: Namespace) -> None:
30
- """Purge the local devservices state and cache and remove all devservices containers and volumes."""
31
- console = Console()
32
-
33
- if os.path.exists(DEVSERVICES_CACHE_DIR):
34
- try:
35
- shutil.rmtree(DEVSERVICES_CACHE_DIR)
36
- except PermissionError as e:
37
- console.failure(f"Failed to purge cache: {e}")
38
- exit(1)
39
- state = State()
40
- state.clear_state()
41
-
42
- try:
43
- devservices_containers = get_matching_containers(
44
- [DEVSERVICES_ORCHESTRATOR_LABEL]
45
- )
46
- except DockerDaemonNotRunningError as e:
47
- console.warning(str(e))
48
- return
49
- except DockerError as de:
50
- console.failure(f"Failed to get devservices containers {de.stderr}")
51
- exit(1)
52
-
53
- try:
54
- devservices_volumes = get_volumes_for_containers(devservices_containers)
55
- except DockerError as e:
56
- console.failure(f"Failed to get devservices volumes {e.stderr}")
57
- exit(1)
58
-
59
- with Status(
60
- lambda: console.warning("Stopping all devservices containers"),
61
- lambda: console.success("All devservices containers have been stopped"),
62
- ):
63
- try:
64
- stop_containers(devservices_containers, should_remove=True)
65
- except DockerError as e:
66
- console.failure(f"Failed to stop devservices containers {e.stderr}")
67
- exit(1)
68
-
69
- console.warning("Removing any devservices docker volumes")
70
- if len(devservices_volumes) == 0:
71
- console.success("No devservices volumes found to remove")
72
- else:
73
- try:
74
- remove_docker_resources("volume", list(devservices_volumes))
75
- console.success("All devservices volumes removed")
76
- except DockerError as e:
77
- # We don't want to exit here since we still want to try to remove the networks
78
- console.failure(f"Failed to remove devservices volumes {e.stderr}")
79
-
80
- console.warning("Removing any devservices networks")
81
- try:
82
- devservices_networks = get_matching_networks(DOCKER_NETWORK_NAME)
83
- except DockerError as e:
84
- console.failure(f"Failed to get devservices networks {e.stderr}")
85
- exit(1)
86
- if len(devservices_networks) == 0:
87
- console.success("No devservices networks found to remove")
88
- else:
89
- try:
90
- remove_docker_resources("network", devservices_networks)
91
- console.success("All devservices networks removed")
92
- except DockerError as e:
93
- console.failure(f"Failed to remove devservices networks {e.stderr}")
94
- exit(1)
95
-
96
- console.success("The local devservices cache and state has been purged")
File without changes
File without changes