devservices 0.0.4__tar.gz → 0.0.5__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 (51) hide show
  1. {devservices-0.0.4 → devservices-0.0.5}/PKG-INFO +1 -1
  2. {devservices-0.0.4 → devservices-0.0.5}/README.md +2 -2
  3. {devservices-0.0.4 → devservices-0.0.5}/devservices/commands/list_services.py +5 -4
  4. {devservices-0.0.4 → devservices-0.0.5}/devservices/commands/logs.py +16 -2
  5. devservices-0.0.5/devservices/commands/purge.py +25 -0
  6. {devservices-0.0.4 → devservices-0.0.5}/devservices/commands/start.py +13 -1
  7. {devservices-0.0.4 → devservices-0.0.5}/devservices/commands/status.py +14 -4
  8. {devservices-0.0.4 → devservices-0.0.5}/devservices/commands/stop.py +14 -0
  9. {devservices-0.0.4 → devservices-0.0.5}/devservices/constants.py +2 -0
  10. {devservices-0.0.4 → devservices-0.0.5}/devservices/exceptions.py +20 -2
  11. {devservices-0.0.4 → devservices-0.0.5}/devservices/main.py +12 -1
  12. {devservices-0.0.4 → devservices-0.0.5}/devservices/utils/dependencies.py +24 -1
  13. devservices-0.0.5/devservices/utils/docker.py +19 -0
  14. {devservices-0.0.4 → devservices-0.0.5}/devservices/utils/docker_compose.py +15 -21
  15. devservices-0.0.5/devservices/utils/state.py +81 -0
  16. {devservices-0.0.4 → devservices-0.0.5}/devservices.egg-info/PKG-INFO +1 -1
  17. {devservices-0.0.4 → devservices-0.0.5}/devservices.egg-info/SOURCES.txt +6 -1
  18. {devservices-0.0.4 → devservices-0.0.5}/pyproject.toml +1 -1
  19. {devservices-0.0.4 → devservices-0.0.5}/tests/commands/test_list_services.py +9 -14
  20. devservices-0.0.5/tests/commands/test_purge.py +35 -0
  21. {devservices-0.0.4 → devservices-0.0.5}/tests/commands/test_start.py +13 -2
  22. {devservices-0.0.4 → devservices-0.0.5}/tests/commands/test_stop.py +25 -5
  23. devservices-0.0.5/tests/conftest.py +10 -0
  24. {devservices-0.0.4 → devservices-0.0.5}/tests/utils/test_dependencies.py +227 -0
  25. {devservices-0.0.4 → devservices-0.0.5}/tests/utils/test_docker_compose.py +87 -24
  26. devservices-0.0.5/tests/utils/test_state.py +54 -0
  27. devservices-0.0.4/tests/conftest.py +0 -0
  28. {devservices-0.0.4 → devservices-0.0.5}/LICENSE.md +0 -0
  29. {devservices-0.0.4 → devservices-0.0.5}/devservices/__init__.py +0 -0
  30. {devservices-0.0.4 → devservices-0.0.5}/devservices/commands/__init__.py +0 -0
  31. {devservices-0.0.4 → devservices-0.0.5}/devservices/commands/check_for_update.py +0 -0
  32. {devservices-0.0.4 → devservices-0.0.5}/devservices/commands/list_dependencies.py +0 -0
  33. {devservices-0.0.4 → devservices-0.0.5}/devservices/commands/update.py +0 -0
  34. {devservices-0.0.4 → devservices-0.0.5}/devservices/configs/service_config.py +0 -0
  35. {devservices-0.0.4 → devservices-0.0.5}/devservices/utils/__init__.py +0 -0
  36. {devservices-0.0.4 → devservices-0.0.5}/devservices/utils/console.py +0 -0
  37. {devservices-0.0.4 → devservices-0.0.5}/devservices/utils/devenv.py +0 -0
  38. {devservices-0.0.4 → devservices-0.0.5}/devservices/utils/file_lock.py +0 -0
  39. {devservices-0.0.4 → devservices-0.0.5}/devservices/utils/install_binary.py +0 -0
  40. {devservices-0.0.4 → devservices-0.0.5}/devservices/utils/services.py +0 -0
  41. {devservices-0.0.4 → devservices-0.0.5}/devservices.egg-info/dependency_links.txt +0 -0
  42. {devservices-0.0.4 → devservices-0.0.5}/devservices.egg-info/entry_points.txt +0 -0
  43. {devservices-0.0.4 → devservices-0.0.5}/devservices.egg-info/requires.txt +0 -0
  44. {devservices-0.0.4 → devservices-0.0.5}/devservices.egg-info/top_level.txt +0 -0
  45. {devservices-0.0.4 → devservices-0.0.5}/setup.cfg +0 -0
  46. {devservices-0.0.4 → devservices-0.0.5}/testing/__init__.py +0 -0
  47. {devservices-0.0.4 → devservices-0.0.5}/testing/utils.py +0 -0
  48. {devservices-0.0.4 → devservices-0.0.5}/tests/__init__.py +0 -0
  49. {devservices-0.0.4 → devservices-0.0.5}/tests/commands/test_update.py +0 -0
  50. {devservices-0.0.4 → devservices-0.0.5}/tests/configs/test_service_config.py +0 -0
  51. {devservices-0.0.4 → devservices-0.0.5}/tests/utils/test_install_binary.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devservices
3
- Version: 0.0.4
3
+ Version: 0.0.5
4
4
  Requires-Python: >=3.10
5
5
  License-File: LICENSE.md
6
6
  Requires-Dist: pyyaml
@@ -13,14 +13,14 @@ A system-wide installation can be done by downloading the binary of the latest r
13
13
  ```
14
14
  PLATFORM=darwin # Options: darwin/linux
15
15
  INSTALL_DIR="$HOME/.local/bin"
16
- curl -L "https://github.com/getsentry/devservices/releases/download/0.0.4/devservices-$PLATFORM" -o "$INSTALL_DIR/devservices"
16
+ curl -L "https://github.com/getsentry/devservices/releases/download/0.0.5/devservices-$PLATFORM" -o "$INSTALL_DIR/devservices"
17
17
  chmod +x "$INSTALL_DIR/devservices"
18
18
  ```
19
19
 
20
20
  Alternatively, if the repository you're working in has a python virtualenv, you can simply add this to the requirements-dev.txt:
21
21
 
22
22
  ```
23
- devservices==0.0.4
23
+ devservices==0.0.5
24
24
  ```
25
25
  ## Usage
26
26
 
@@ -5,8 +5,8 @@ from argparse import ArgumentParser
5
5
  from argparse import Namespace
6
6
 
7
7
  from devservices.utils.devenv import get_coderoot
8
- from devservices.utils.docker_compose import get_active_docker_compose_projects
9
8
  from devservices.utils.services import get_local_services
9
+ from devservices.utils.state import State
10
10
 
11
11
 
12
12
  def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
@@ -28,14 +28,15 @@ def list_services(args: Namespace) -> None:
28
28
  # Get all of the services installed locally
29
29
  coderoot = get_coderoot()
30
30
  services = get_local_services(coderoot)
31
- running_projects = get_active_docker_compose_projects()
31
+ state = State()
32
+ running_services = state.get_started_services()
32
33
 
33
34
  if not services:
34
35
  print("No services found")
35
36
  return
36
37
 
37
38
  services_to_show = (
38
- services if args.all else [s for s in services if s.name in running_projects]
39
+ services if args.all else [s for s in services if s.name in running_services]
39
40
  )
40
41
 
41
42
  if args.all:
@@ -44,7 +45,7 @@ def list_services(args: Namespace) -> None:
44
45
  print("Running services:")
45
46
 
46
47
  for service in services_to_show:
47
- status = "running" if service.name in running_projects else "stopped"
48
+ status = "running" if service.name in running_services else "stopped"
48
49
  print(f"- {service.name}")
49
50
  print(f" status: {status}")
50
51
  print(f" location: {service.repo_path}")
@@ -5,9 +5,12 @@ from argparse import _SubParsersAction
5
5
  from argparse import ArgumentParser
6
6
  from argparse import Namespace
7
7
 
8
+ from devservices.constants import MAX_LOG_LINES
9
+ from devservices.exceptions import DependencyError
8
10
  from devservices.exceptions import DockerComposeError
9
11
  from devservices.utils.docker_compose import run_docker_compose_command
10
12
  from devservices.utils.services import find_matching_service
13
+ from devservices.utils.state import State
11
14
 
12
15
 
13
16
  def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
@@ -35,11 +38,22 @@ def logs(args: Namespace) -> None:
35
38
  mode_to_use = "default"
36
39
  mode_dependencies = modes[mode_to_use]
37
40
 
41
+ state = State()
42
+ running_services = state.get_started_services()
43
+ if service_name not in running_services:
44
+ print(f"Service {service_name} is not running")
45
+ return
46
+
38
47
  try:
39
- logs = run_docker_compose_command(service, "logs", mode_dependencies)
48
+ logs_output = run_docker_compose_command(
49
+ service, "logs", mode_dependencies, options=["-n", MAX_LOG_LINES]
50
+ )
51
+ except DependencyError as de:
52
+ print(str(de))
53
+ exit(1)
40
54
  except DockerComposeError as dce:
41
55
  print(f"Failed to get logs for {service.name}: {dce.stderr}")
42
56
  exit(1)
43
- for log in logs:
57
+ for log in logs_output:
44
58
  sys.stdout.write(log.stdout)
45
59
  sys.stdout.flush()
@@ -0,0 +1,25 @@
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
+
11
+
12
+ def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
13
+ parser = subparsers.add_parser("purge", help="Purge the local devservices cache")
14
+ parser.set_defaults(func=purge)
15
+
16
+
17
+ def purge(args: Namespace) -> None:
18
+ """Purge the local devservices cache."""
19
+ if os.path.exists(DEVSERVICES_CACHE_DIR):
20
+ try:
21
+ shutil.rmtree(DEVSERVICES_CACHE_DIR)
22
+ except PermissionError as e:
23
+ print(f"Failed to purge cache: {e}")
24
+ exit(1)
25
+ print("The local devservices cache has been purged")
@@ -4,10 +4,12 @@ from argparse import _SubParsersAction
4
4
  from argparse import ArgumentParser
5
5
  from argparse import Namespace
6
6
 
7
+ from devservices.exceptions import DependencyError
7
8
  from devservices.exceptions import DockerComposeError
8
9
  from devservices.utils.console import Status
9
10
  from devservices.utils.docker_compose import run_docker_compose_command
10
11
  from devservices.utils.services import find_matching_service
12
+ from devservices.utils.state import State
11
13
 
12
14
 
13
15
  def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
@@ -35,8 +37,18 @@ def start(args: Namespace) -> None:
35
37
  with Status(f"Starting {service.name}", f"{service.name} started") as status:
36
38
  try:
37
39
  run_docker_compose_command(
38
- service, "up", mode_dependencies, ["-d"], force_update_dependencies=True
40
+ service,
41
+ "up",
42
+ mode_dependencies,
43
+ ["-d"],
44
+ force_update_dependencies=True,
39
45
  )
46
+ except DependencyError as de:
47
+ status.print(str(de))
48
+ exit(1)
40
49
  except DockerComposeError as dce:
41
50
  status.print(f"Failed to start {service.name}: {dce.stderr}")
42
51
  exit(1)
52
+ # TODO: We should factor in healthchecks here before marking service as running
53
+ state = State()
54
+ state.add_started_service(service.name, mode_to_start)
@@ -6,6 +6,7 @@ from argparse import _SubParsersAction
6
6
  from argparse import ArgumentParser
7
7
  from argparse import Namespace
8
8
 
9
+ from devservices.exceptions import DependencyError
9
10
  from devservices.exceptions import DockerComposeError
10
11
  from devservices.utils.docker_compose import run_docker_compose_command
11
12
  from devservices.utils.services import find_matching_service
@@ -33,11 +34,13 @@ def format_status_output(status_json: str) -> str:
33
34
  service = json.loads(service_status)
34
35
  name = service["Service"]
35
36
  state = service["State"]
37
+ container_name = service["Name"]
36
38
  health = service.get("Health", "N/A")
37
39
  ports = service.get("Publishers", [])
38
40
  running_for = service.get("RunningFor", "N/A")
39
41
 
40
42
  output.append(f"{name}")
43
+ output.append(f"Container: {container_name}")
41
44
  output.append(f"Status: {state}")
42
45
  output.append(f"Health: {health}")
43
46
  output.append(f"Uptime: {running_for}")
@@ -71,18 +74,25 @@ def status(args: Namespace) -> None:
71
74
  mode_dependencies = modes[mode_to_view]
72
75
 
73
76
  try:
74
- status_jsons = run_docker_compose_command(
77
+ status_json_results = run_docker_compose_command(
75
78
  service, "ps", mode_dependencies, options=["--format", "json"]
76
79
  )
80
+ except DependencyError as de:
81
+ print(str(de))
82
+ exit(1)
77
83
  except DockerComposeError as dce:
78
84
  print(f"Failed to get status for {service.name}: {dce.stderr}")
79
85
  exit(1)
80
- # If the service is not running, the status_json will be empty
81
- if len(status_jsons) == 0:
86
+
87
+ # Filter out empty stdout to help us determine if the service is running
88
+ status_json_results = [
89
+ status_json for status_json in status_json_results if status_json.stdout
90
+ ]
91
+ if len(status_json_results) == 0:
82
92
  print(f"{service.name} is not running")
83
93
  return
84
94
  output = f"Service: {service.name}\n\n"
85
- for status_json in status_jsons:
95
+ for status_json in status_json_results:
86
96
  output += format_status_output(status_json.stdout)
87
97
  output += "=" * LINE_LENGTH
88
98
  sys.stdout.write(output + "\n")
@@ -4,10 +4,12 @@ from argparse import _SubParsersAction
4
4
  from argparse import ArgumentParser
5
5
  from argparse import Namespace
6
6
 
7
+ from devservices.exceptions import DependencyError
7
8
  from devservices.exceptions import DockerComposeError
8
9
  from devservices.utils.console import Status
9
10
  from devservices.utils.docker_compose import run_docker_compose_command
10
11
  from devservices.utils.services import find_matching_service
12
+ from devservices.utils.state import State
11
13
 
12
14
 
13
15
  def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
@@ -31,10 +33,22 @@ def stop(args: Namespace) -> None:
31
33
  # TODO: allow custom modes to be used
32
34
  mode_to_stop = "default"
33
35
  mode_dependencies = modes[mode_to_stop]
36
+ state = State()
37
+ started_services = state.get_started_services()
38
+ if service.name not in started_services:
39
+ print(f"{service.name} is not running")
40
+ exit(0)
34
41
 
35
42
  with Status(f"Stopping {service.name}", f"{service.name} stopped") as status:
36
43
  try:
37
44
  run_docker_compose_command(service, "down", mode_dependencies)
45
+ except DependencyError as de:
46
+ status.print(str(de))
47
+ exit(1)
38
48
  except DockerComposeError as dce:
39
49
  status.print(f"Failed to stop {service.name}: {dce.stderr}")
40
50
  exit(1)
51
+
52
+ # TODO: We should factor in healthchecks here before marking service as stopped
53
+ state = State()
54
+ state.remove_started_service(service.name)
@@ -11,6 +11,7 @@ DEVSERVICES_CACHE_DIR = os.path.expanduser("~/.cache/sentry-devservices")
11
11
  DEVSERVICES_LOCAL_DIR = os.path.expanduser("~/.local/share/sentry-devservices")
12
12
  DEVSERVICES_DEPENDENCIES_CACHE_DIR = os.path.join(DEVSERVICES_CACHE_DIR, "dependencies")
13
13
  DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY = "DEVSERVICES_DEPENDENCIES_CACHE_DIR"
14
+ STATE_DB_FILE = os.path.join(DEVSERVICES_LOCAL_DIR, "state")
14
15
 
15
16
  DEPENDENCY_CONFIG_VERSION = "v1"
16
17
  DEPENDENCY_GIT_PARTIAL_CLONE_CONFIG_OPTIONS = {
@@ -22,3 +23,4 @@ DEPENDENCY_GIT_PARTIAL_CLONE_CONFIG_OPTIONS = {
22
23
  DOCKER_COMPOSE_DOWNLOAD_URL = "https://github.com/docker/compose/releases/download"
23
24
  DEVSERVICES_DOWNLOAD_URL = "https://github.com/getsentry/devservices/releases/download"
24
25
  BINARY_PERMISSIONS = 0o755
26
+ MAX_LOG_LINES = "100"
@@ -43,6 +43,12 @@ class DevservicesUpdateError(BinaryInstallError):
43
43
  pass
44
44
 
45
45
 
46
+ class DockerDaemonNotRunningError(Exception):
47
+ """Raised when the Docker daemon is not running."""
48
+
49
+ pass
50
+
51
+
46
52
  class DockerComposeInstallationError(BinaryInstallError):
47
53
  """Raised when the Docker Compose installation fails."""
48
54
 
@@ -67,17 +73,29 @@ class DependencyError(Exception):
67
73
  self.repo_link = repo_link
68
74
  self.branch = branch
69
75
 
76
+ def __str__(self) -> str:
77
+ return f"DependencyError: {self.repo_name} ({self.repo_link}) on {self.branch}"
78
+
79
+
80
+ class UnableToCloneDependencyError(DependencyError):
81
+ """Raised when a dependency is unable to be cloned."""
82
+
83
+ def __str__(self) -> str:
84
+ return f"Unable to clone dependency: {self.repo_name} ({self.repo_link}) on {self.branch}"
85
+
70
86
 
71
87
  class InvalidDependencyConfigError(DependencyError):
72
88
  """Raised when a dependency's config is invalid."""
73
89
 
74
- pass
90
+ def __str__(self) -> str:
91
+ return f"Invalid config for dependency: {self.repo_name} ({self.repo_link}) on {self.branch}"
75
92
 
76
93
 
77
94
  class DependencyNotInstalledError(DependencyError):
78
95
  """Raised when a dependency is not installed correctly."""
79
96
 
80
- pass
97
+ def __str__(self) -> str:
98
+ return f"Dependency not installed correctly: {self.repo_name} ({self.repo_link}) on {self.branch}"
81
99
 
82
100
 
83
101
  class GitConfigError(Exception):
@@ -11,11 +11,14 @@ from sentry_sdk.integrations.argv import ArgvIntegration
11
11
  from devservices.commands import list_dependencies
12
12
  from devservices.commands import list_services
13
13
  from devservices.commands import logs
14
+ from devservices.commands import purge
14
15
  from devservices.commands import start
15
16
  from devservices.commands import status
16
17
  from devservices.commands import stop
17
18
  from devservices.commands import update
18
19
  from devservices.commands.check_for_update import check_for_update
20
+ from devservices.exceptions import DockerComposeInstallationError
21
+ from devservices.exceptions import DockerDaemonNotRunningError
19
22
  from devservices.utils.docker_compose import check_docker_compose_version
20
23
 
21
24
  sentry_environment = (
@@ -41,7 +44,14 @@ def cleanup() -> None:
41
44
 
42
45
 
43
46
  def main() -> None:
44
- check_docker_compose_version()
47
+ try:
48
+ check_docker_compose_version()
49
+ except DockerDaemonNotRunningError as e:
50
+ print(e)
51
+ exit(1)
52
+ except DockerComposeInstallationError:
53
+ print("Failed to ensure docker compose is installed and up-to-date")
54
+ exit(1)
45
55
  parser = argparse.ArgumentParser(
46
56
  prog="devservices",
47
57
  description="CLI tool for managing service dependencies.",
@@ -61,6 +71,7 @@ def main() -> None:
61
71
  status.add_parser(subparsers)
62
72
  logs.add_parser(subparsers)
63
73
  update.add_parser(subparsers)
74
+ purge.add_parser(subparsers)
64
75
 
65
76
  args = parser.parse_args()
66
77
 
@@ -25,7 +25,11 @@ from devservices.exceptions import DependencyError
25
25
  from devservices.exceptions import DependencyNotInstalledError
26
26
  from devservices.exceptions import FailedToSetGitConfigError
27
27
  from devservices.exceptions import InvalidDependencyConfigError
28
+ from devservices.exceptions import UnableToCloneDependencyError
28
29
  from devservices.utils.file_lock import lock
30
+ from devservices.utils.services import find_matching_service
31
+ from devservices.utils.services import Service
32
+ from devservices.utils.state import State
29
33
 
30
34
 
31
35
  @dataclass(frozen=True)
@@ -132,6 +136,25 @@ def verify_local_dependencies(dependencies: list[Dependency]) -> bool:
132
136
  )
133
137
 
134
138
 
139
+ def get_non_shared_remote_dependencies(
140
+ service_to_stop: Service, remote_dependencies: set[InstalledRemoteDependency]
141
+ ) -> set[InstalledRemoteDependency]:
142
+ state = State()
143
+ started_services = state.get_started_services()
144
+ # We don't care about the remote dependencies of the service we are stopping
145
+ started_services.remove(service_to_stop.name)
146
+ other_running_remote_dependencies: set[InstalledRemoteDependency] = set()
147
+ for service_name in started_services:
148
+ service = find_matching_service(service_name)
149
+ # TODO: There is an edge case here where there is a shared remote dependency with different modes
150
+ other_running_remote_dependencies = other_running_remote_dependencies.union(
151
+ get_installed_remote_dependencies(
152
+ list(service.config.dependencies.values())
153
+ )
154
+ )
155
+ return remote_dependencies.difference(other_running_remote_dependencies)
156
+
157
+
135
158
  def get_installed_remote_dependencies(
136
159
  dependencies: list[Dependency],
137
160
  ) -> set[InstalledRemoteDependency]:
@@ -344,7 +367,7 @@ def _checkout_dependency(
344
367
  cwd=temp_dir,
345
368
  )
346
369
  except subprocess.CalledProcessError as e:
347
- raise DependencyError(
370
+ raise UnableToCloneDependencyError(
348
371
  repo_name=dependency.repo_name,
349
372
  repo_link=dependency.repo_link,
350
373
  branch=dependency.branch,
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ import subprocess
4
+
5
+ from devservices.exceptions import DockerDaemonNotRunningError
6
+
7
+
8
+ def check_docker_daemon_running() -> None:
9
+ try:
10
+ subprocess.run(
11
+ ["docker", "info"],
12
+ capture_output=True,
13
+ text=True,
14
+ check=True,
15
+ )
16
+ except subprocess.CalledProcessError as e:
17
+ raise DockerDaemonNotRunningError(
18
+ "Unable to connect to the docker daemon. Is the docker daemon running?"
19
+ ) from e
@@ -22,28 +22,15 @@ from devservices.exceptions import BinaryInstallError
22
22
  from devservices.exceptions import DockerComposeError
23
23
  from devservices.exceptions import DockerComposeInstallationError
24
24
  from devservices.utils.dependencies import get_installed_remote_dependencies
25
+ from devservices.utils.dependencies import get_non_shared_remote_dependencies
25
26
  from devservices.utils.dependencies import install_dependencies
26
27
  from devservices.utils.dependencies import InstalledRemoteDependency
27
28
  from devservices.utils.dependencies import verify_local_dependencies
29
+ from devservices.utils.docker import check_docker_daemon_running
28
30
  from devservices.utils.install_binary import install_binary
29
31
  from devservices.utils.services import Service
30
32
 
31
33
 
32
- def get_active_docker_compose_projects() -> list[str]:
33
- cmd = ["docker", "compose", "ls", "-q"]
34
- try:
35
- running_projects = subprocess.check_output(cmd, text=True)
36
- except subprocess.CalledProcessError as e:
37
- raise DockerComposeError(
38
- command=" ".join(cmd),
39
- returncode=e.returncode,
40
- stdout=e.stdout,
41
- stderr=e.stderr,
42
- )
43
- # docker compose ls always returns newline delimited string with an extra newline at the end
44
- return running_projects.split("\n")[:-1]
45
-
46
-
47
34
  def install_docker_compose() -> None:
48
35
  # Determine the platform
49
36
  system = platform.system()
@@ -104,11 +91,12 @@ def install_docker_compose() -> None:
104
91
 
105
92
 
106
93
  def check_docker_compose_version() -> None:
107
- cmd = ["docker", "compose", "version", "--short"]
94
+ # Throw an error if docker daemon isn't running
95
+ check_docker_daemon_running()
108
96
  try:
109
97
  # Run the docker compose version command
110
98
  result = subprocess.run(
111
- cmd,
99
+ ["docker", "compose", "version", "--short"],
112
100
  capture_output=True,
113
101
  text=True,
114
102
  check=True,
@@ -224,11 +212,12 @@ def _get_docker_compose_commands_to_run(
224
212
  service_config_file_path, current_env
225
213
  )
226
214
  services_to_use = non_remote_services.intersection(set(mode_dependencies))
227
- docker_compose_commands.append(
228
- create_docker_compose_command(
229
- service.name, service_config_file_path, services_to_use
215
+ if len(services_to_use) > 0:
216
+ docker_compose_commands.append(
217
+ create_docker_compose_command(
218
+ service.name, service_config_file_path, services_to_use
219
+ )
230
220
  )
231
- )
232
221
  return docker_compose_commands
233
222
 
234
223
 
@@ -250,6 +239,11 @@ def run_docker_compose_command(
250
239
  remote_dependencies = install_dependencies(dependencies)
251
240
  else:
252
241
  remote_dependencies = get_installed_remote_dependencies(dependencies)
242
+ # TODO: Refactor this to be more generic instead of having a one-off case for stopping
243
+ if command == "down":
244
+ remote_dependencies = get_non_shared_remote_dependencies(
245
+ service, remote_dependencies
246
+ )
253
247
  relative_local_dependency_directory = os.path.relpath(
254
248
  os.path.join(DEVSERVICES_DEPENDENCIES_CACHE_DIR, DEPENDENCY_CONFIG_VERSION),
255
249
  service.repo_path,
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sqlite3
5
+
6
+ from devservices.constants import DEVSERVICES_LOCAL_DIR
7
+ from devservices.constants import STATE_DB_FILE
8
+
9
+
10
+ class State:
11
+ _instance: State | None = None
12
+ state_db_file: str
13
+ conn: sqlite3.Connection
14
+
15
+ def __new__(cls) -> State:
16
+ if cls._instance is None:
17
+ cls._instance = super(State, cls).__new__(cls)
18
+ if not os.path.exists(DEVSERVICES_LOCAL_DIR):
19
+ os.makedirs(DEVSERVICES_LOCAL_DIR)
20
+ cls._instance.state_db_file = STATE_DB_FILE
21
+ cls._instance.conn = sqlite3.connect(cls._instance.state_db_file)
22
+ cls._instance.initialize_database()
23
+ return cls._instance
24
+
25
+ def initialize_database(self) -> None:
26
+ cursor = self.conn.cursor()
27
+ cursor.execute(
28
+ """
29
+ CREATE TABLE IF NOT EXISTS started_services (
30
+ service_name TEXT PRIMARY KEY,
31
+ mode TEXT,
32
+ timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
33
+ )
34
+ """
35
+ )
36
+ self.conn.commit()
37
+
38
+ def add_started_service(self, service_name: str, mode: str) -> None:
39
+ cursor = self.conn.cursor()
40
+ started_services = self.get_started_services()
41
+ if service_name in started_services:
42
+ return
43
+ cursor.execute(
44
+ """
45
+ INSERT INTO started_services (service_name, mode) VALUES (?, ?)
46
+ """,
47
+ (service_name, mode),
48
+ )
49
+ self.conn.commit()
50
+
51
+ def remove_started_service(self, service_name: str) -> None:
52
+ cursor = self.conn.cursor()
53
+ cursor.execute(
54
+ """
55
+ DELETE FROM started_services WHERE service_name = ?
56
+ """,
57
+ (service_name,),
58
+ )
59
+ self.conn.commit()
60
+
61
+ def get_started_services(self) -> list[str]:
62
+ cursor = self.conn.cursor()
63
+ cursor.execute(
64
+ """
65
+ SELECT service_name FROM started_services
66
+ """
67
+ )
68
+ return [row[0] for row in cursor.fetchall()]
69
+
70
+ def get_mode_for_service(self, service_name: str) -> str | None:
71
+ cursor = self.conn.cursor()
72
+ cursor.execute(
73
+ """
74
+ SELECT mode FROM started_services WHERE service_name = ?
75
+ """,
76
+ (service_name,),
77
+ )
78
+ result = cursor.fetchone()
79
+ if result is None:
80
+ return None
81
+ return str(result[0])
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devservices
3
- Version: 0.0.4
3
+ Version: 0.0.5
4
4
  Requires-Python: >=3.10
5
5
  License-File: LICENSE.md
6
6
  Requires-Dist: pyyaml
@@ -17,6 +17,7 @@ devservices/commands/check_for_update.py
17
17
  devservices/commands/list_dependencies.py
18
18
  devservices/commands/list_services.py
19
19
  devservices/commands/logs.py
20
+ devservices/commands/purge.py
20
21
  devservices/commands/start.py
21
22
  devservices/commands/status.py
22
23
  devservices/commands/stop.py
@@ -26,19 +27,23 @@ devservices/utils/__init__.py
26
27
  devservices/utils/console.py
27
28
  devservices/utils/dependencies.py
28
29
  devservices/utils/devenv.py
30
+ devservices/utils/docker.py
29
31
  devservices/utils/docker_compose.py
30
32
  devservices/utils/file_lock.py
31
33
  devservices/utils/install_binary.py
32
34
  devservices/utils/services.py
35
+ devservices/utils/state.py
33
36
  testing/__init__.py
34
37
  testing/utils.py
35
38
  tests/__init__.py
36
39
  tests/conftest.py
37
40
  tests/commands/test_list_services.py
41
+ tests/commands/test_purge.py
38
42
  tests/commands/test_start.py
39
43
  tests/commands/test_stop.py
40
44
  tests/commands/test_update.py
41
45
  tests/configs/test_service_config.py
42
46
  tests/utils/test_dependencies.py
43
47
  tests/utils/test_docker_compose.py
44
- tests/utils/test_install_binary.py
48
+ tests/utils/test_install_binary.py
49
+ tests/utils/test_state.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devservices"
7
- version = "0.0.4"
7
+ version = "0.0.5"
8
8
  # 3.10 is just for internal pypi compat
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [
@@ -7,20 +7,19 @@ from unittest import mock
7
7
  import pytest
8
8
 
9
9
  from devservices.commands.list_services import list_services
10
+ from devservices.utils.state import State
10
11
  from testing.utils import create_config_file
11
12
 
12
13
 
13
- @mock.patch(
14
- "devservices.utils.docker_compose.subprocess.run",
15
- return_value=Namespace(stdout="\nexample-service\n"),
16
- )
17
14
  def test_list_running_services(
18
- mock_run: mock.Mock, tmp_path: Path, capsys: pytest.CaptureFixture[str]
15
+ tmp_path: Path, capsys: pytest.CaptureFixture[str]
19
16
  ) -> None:
20
17
  with mock.patch(
21
18
  "devservices.commands.list_services.get_coderoot",
22
19
  return_value=str(tmp_path / "code"),
23
- ):
20
+ ), mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")):
21
+ state = State()
22
+ state.add_started_service("example-service", "default")
24
23
  config = {
25
24
  "x-sentry-service-config": {
26
25
  "version": 0.1,
@@ -52,17 +51,13 @@ def test_list_running_services(
52
51
  )
53
52
 
54
53
 
55
- @mock.patch(
56
- "devservices.utils.docker_compose.subprocess.run",
57
- return_value=Namespace(stdout="\nexample-service\n"),
58
- )
59
- def test_list_all_services(
60
- mock_run: mock.Mock, tmp_path: Path, capsys: pytest.CaptureFixture[str]
61
- ) -> None:
54
+ def test_list_all_services(tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
62
55
  with mock.patch(
63
56
  "devservices.commands.list_services.get_coderoot",
64
57
  return_value=str(tmp_path / "code"),
65
- ):
58
+ ), mock.patch("devservices.utils.state.STATE_DB_FILE", str(tmp_path / "state")):
59
+ state = State()
60
+ state.add_started_service("example-service", "default")
66
61
  config = {
67
62
  "x-sentry-service-config": {
68
63
  "version": 0.1,