devservices 0.0.1__tar.gz → 0.0.2__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 (36) hide show
  1. {devservices-0.0.1 → devservices-0.0.2}/PKG-INFO +2 -2
  2. devservices-0.0.2/devservices/__init__.py +4 -0
  3. {devservices-0.0.1/src → devservices-0.0.2/devservices}/commands/list_dependencies.py +1 -1
  4. {devservices-0.0.1/src → devservices-0.0.2/devservices}/commands/list_services.py +2 -2
  5. {devservices-0.0.1/src → devservices-0.0.2/devservices}/commands/logs.py +5 -5
  6. {devservices-0.0.1/src → devservices-0.0.2/devservices}/commands/start.py +6 -6
  7. {devservices-0.0.1/src → devservices-0.0.2/devservices}/commands/status.py +6 -7
  8. {devservices-0.0.1/src → devservices-0.0.2/devservices}/commands/stop.py +6 -6
  9. {devservices-0.0.1/src → devservices-0.0.2/devservices}/configs/service_config.py +5 -5
  10. devservices-0.0.2/devservices/constants.py +4 -0
  11. devservices-0.0.2/devservices/exceptions.py +41 -0
  12. devservices-0.0.2/devservices/main.py +57 -0
  13. {devservices-0.0.1/src → devservices-0.0.2/devservices}/utils/docker_compose.py +1 -1
  14. {devservices-0.0.1/src → devservices-0.0.2/devservices}/utils/services.py +8 -8
  15. {devservices-0.0.1/src → devservices-0.0.2}/devservices.egg-info/PKG-INFO +2 -2
  16. devservices-0.0.2/devservices.egg-info/SOURCES.txt +32 -0
  17. devservices-0.0.2/devservices.egg-info/entry_points.txt +2 -0
  18. devservices-0.0.2/devservices.egg-info/top_level.txt +4 -0
  19. {devservices-0.0.1 → devservices-0.0.2}/pyproject.toml +7 -7
  20. devservices-0.0.2/tests/__init__.py +0 -0
  21. devservices-0.0.2/tests/commands/test_start.py +97 -0
  22. devservices-0.0.2/tests/commands/test_stop.py +97 -0
  23. devservices-0.0.2/tests/configs/test_service_config.py +323 -0
  24. devservices-0.0.2/tests/conftest.py +0 -0
  25. devservices-0.0.1/src/devservices.egg-info/SOURCES.txt +0 -23
  26. devservices-0.0.1/src/devservices.egg-info/entry_points.txt +0 -2
  27. devservices-0.0.1/src/devservices.egg-info/top_level.txt +0 -3
  28. {devservices-0.0.1 → devservices-0.0.2}/README.md +0 -0
  29. {devservices-0.0.1/src → devservices-0.0.2/devservices}/commands/__init__.py +0 -0
  30. {devservices-0.0.1/src → devservices-0.0.2/devservices}/utils/__init__.py +0 -0
  31. {devservices-0.0.1/src → devservices-0.0.2/devservices}/utils/console.py +0 -0
  32. {devservices-0.0.1/src → devservices-0.0.2/devservices}/utils/devenv.py +0 -0
  33. {devservices-0.0.1/src → devservices-0.0.2}/devservices.egg-info/dependency_links.txt +0 -0
  34. {devservices-0.0.1/src → devservices-0.0.2}/devservices.egg-info/requires.txt +0 -0
  35. {devservices-0.0.1 → devservices-0.0.2}/setup.cfg +0 -0
  36. {devservices-0.0.1 → devservices-0.0.2}/tests/testutils.py +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devservices
3
- Version: 0.0.1
4
- Requires-Python: >=3.12
3
+ Version: 0.0.2
4
+ Requires-Python: >=3.10
5
5
  Requires-Dist: pyyaml
6
6
  Requires-Dist: sentry-devenv
7
7
  Provides-Extra: dev
@@ -0,0 +1,4 @@
1
+ """
2
+ DevServices CLI tool for managing Docker Compose services.
3
+ """
4
+ from __future__ import annotations
@@ -4,7 +4,7 @@ from argparse import _SubParsersAction
4
4
  from argparse import ArgumentParser
5
5
  from argparse import Namespace
6
6
 
7
- from utils.services import find_matching_service
7
+ from devservices.utils.services import find_matching_service
8
8
 
9
9
 
10
10
  def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
@@ -4,8 +4,8 @@ from argparse import _SubParsersAction
4
4
  from argparse import ArgumentParser
5
5
  from argparse import Namespace
6
6
 
7
- from utils.devenv import get_coderoot
8
- from utils.services import get_local_services
7
+ from devservices.utils.devenv import get_coderoot
8
+ from devservices.utils.services import get_local_services
9
9
 
10
10
 
11
11
  def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
@@ -6,11 +6,11 @@ from argparse import _SubParsersAction
6
6
  from argparse import ArgumentParser
7
7
  from argparse import Namespace
8
8
 
9
- from constants import DEVSERVICES_DIR_NAME
10
- from constants import DOCKER_COMPOSE_FILE_NAME
11
- from exceptions import DockerComposeError
12
- from utils.docker_compose import run_docker_compose_command
13
- from utils.services import find_matching_service
9
+ from devservices.constants import DEVSERVICES_DIR_NAME
10
+ from devservices.constants import DOCKER_COMPOSE_FILE_NAME
11
+ from devservices.exceptions import DockerComposeError
12
+ from devservices.utils.docker_compose import run_docker_compose_command
13
+ from devservices.utils.services import find_matching_service
14
14
 
15
15
 
16
16
  def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
@@ -5,12 +5,12 @@ from argparse import _SubParsersAction
5
5
  from argparse import ArgumentParser
6
6
  from argparse import Namespace
7
7
 
8
- from constants import DEVSERVICES_DIR_NAME
9
- from constants import DOCKER_COMPOSE_FILE_NAME
10
- from exceptions import DockerComposeError
11
- from utils.console import Status
12
- from utils.docker_compose import run_docker_compose_command
13
- from utils.services import find_matching_service
8
+ from devservices.constants import DEVSERVICES_DIR_NAME
9
+ from devservices.constants import DOCKER_COMPOSE_FILE_NAME
10
+ from devservices.exceptions import DockerComposeError
11
+ from devservices.utils.console import Status
12
+ from devservices.utils.docker_compose import run_docker_compose_command
13
+ from devservices.utils.services import find_matching_service
14
14
 
15
15
 
16
16
  def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
@@ -7,11 +7,11 @@ from argparse import _SubParsersAction
7
7
  from argparse import ArgumentParser
8
8
  from argparse import Namespace
9
9
 
10
- from constants import DEVSERVICES_DIR_NAME
11
- from constants import DOCKER_COMPOSE_FILE_NAME
12
- from exceptions import DockerComposeError
13
- from utils.docker_compose import run_docker_compose_command
14
- from utils.services import find_matching_service
10
+ from devservices.constants import DEVSERVICES_DIR_NAME
11
+ from devservices.constants import DOCKER_COMPOSE_FILE_NAME
12
+ from devservices.exceptions import DockerComposeError
13
+ from devservices.utils.docker_compose import run_docker_compose_command
14
+ from devservices.utils.services import find_matching_service
15
15
 
16
16
  LINE_LENGTH = 40
17
17
 
@@ -70,11 +70,10 @@ def status(args: Namespace) -> None:
70
70
  modes = service.config.modes
71
71
  # TODO: allow custom modes to be used
72
72
  mode_to_view = "default"
73
- mode_dependencies = modes[mode_to_view]
73
+ mode_dependencies = " ".join(modes[mode_to_view])
74
74
  service_config_file_path = os.path.join(
75
75
  service.repo_path, DEVSERVICES_DIR_NAME, DOCKER_COMPOSE_FILE_NAME
76
76
  )
77
- mode_dependencies = " ".join(modes[mode_to_view])
78
77
  try:
79
78
  status_json = run_docker_compose_command(
80
79
  f"-f {service_config_file_path} ps {mode_dependencies} --format json"
@@ -5,12 +5,12 @@ from argparse import _SubParsersAction
5
5
  from argparse import ArgumentParser
6
6
  from argparse import Namespace
7
7
 
8
- from constants import DEVSERVICES_DIR_NAME
9
- from constants import DOCKER_COMPOSE_FILE_NAME
10
- from exceptions import DockerComposeError
11
- from utils.console import Status
12
- from utils.docker_compose import run_docker_compose_command
13
- from utils.services import find_matching_service
8
+ from devservices.constants import DEVSERVICES_DIR_NAME
9
+ from devservices.constants import DOCKER_COMPOSE_FILE_NAME
10
+ from devservices.exceptions import DockerComposeError
11
+ from devservices.utils.console import Status
12
+ from devservices.utils.docker_compose import run_docker_compose_command
13
+ from devservices.utils.services import find_matching_service
14
14
 
15
15
 
16
16
  def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
@@ -4,12 +4,12 @@ import os
4
4
  from dataclasses import dataclass
5
5
 
6
6
  import yaml
7
- from constants import DEVSERVICES_DIR_NAME
8
- from constants import DOCKER_COMPOSE_FILE_NAME
9
- from exceptions import ConfigNotFoundError
10
- from exceptions import ConfigParseError
11
- from exceptions import ConfigValidationError
12
7
 
8
+ from devservices.constants import DEVSERVICES_DIR_NAME
9
+ from devservices.constants import DOCKER_COMPOSE_FILE_NAME
10
+ from devservices.exceptions import ConfigNotFoundError
11
+ from devservices.exceptions import ConfigParseError
12
+ from devservices.exceptions import ConfigValidationError
13
13
 
14
14
  VALID_VERSIONS = [0.1]
15
15
 
@@ -0,0 +1,4 @@
1
+ from __future__ import annotations
2
+
3
+ DEVSERVICES_DIR_NAME = "devservices"
4
+ DOCKER_COMPOSE_FILE_NAME = "docker-compose.yml"
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class ServiceNotFoundError(Exception):
5
+ """Raised when a service is not found."""
6
+
7
+ pass
8
+
9
+
10
+ class ConfigError(Exception):
11
+ """Base class for configuration-related errors."""
12
+
13
+ pass
14
+
15
+
16
+ class ConfigNotFoundError(ConfigError):
17
+ """Raised when a configuration file is not found."""
18
+
19
+ pass
20
+
21
+
22
+ class ConfigValidationError(ConfigError):
23
+ """Raised when a configuration file is invalid."""
24
+
25
+ pass
26
+
27
+
28
+ class ConfigParseError(ConfigError):
29
+ """Raised when a configuration file cannot be parsed."""
30
+
31
+ pass
32
+
33
+
34
+ class DockerComposeError(Exception):
35
+ """Base class for Docker Compose related errors."""
36
+
37
+ def __init__(self, command: str, returncode: int, stdout: str, stderr: str):
38
+ self.command = command
39
+ self.returncode = returncode
40
+ self.stdout = stdout
41
+ self.stderr = stderr
@@ -0,0 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import atexit
5
+
6
+ import sentry_sdk
7
+ from sentry_sdk.integrations.argv import ArgvIntegration
8
+
9
+ from devservices.commands import list_dependencies
10
+ from devservices.commands import list_services
11
+ from devservices.commands import logs
12
+ from devservices.commands import start
13
+ from devservices.commands import status
14
+ from devservices.commands import stop
15
+
16
+ sentry_sdk.init(
17
+ dsn="https://56470da7302c16e83141f62f88e46449@o1.ingest.us.sentry.io/4507946704961536",
18
+ traces_sample_rate=1.0,
19
+ profiles_sample_rate=1.0,
20
+ enable_tracing=True,
21
+ integrations=[ArgvIntegration()],
22
+ )
23
+
24
+
25
+ @atexit.register
26
+ def cleanup() -> None:
27
+ sentry_sdk.flush()
28
+
29
+
30
+ def main() -> None:
31
+ parser = argparse.ArgumentParser(
32
+ description="DevServices CLI tool for managing Docker Compose services."
33
+ )
34
+ parser.add_argument("--version", action="version", version="%(prog)s 0.0.1")
35
+
36
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
37
+
38
+ # Add subparsers for each command
39
+ start.add_parser(subparsers)
40
+ stop.add_parser(subparsers)
41
+ list_dependencies.add_parser(subparsers)
42
+ list_services.add_parser(subparsers)
43
+ status.add_parser(subparsers)
44
+ logs.add_parser(subparsers)
45
+
46
+ args = parser.parse_args()
47
+
48
+ if args.command:
49
+ # Call the appropriate function based on the command
50
+ with sentry_sdk.start_transaction(op="command", name=args.command):
51
+ args.func(args)
52
+ else:
53
+ parser.print_help()
54
+
55
+
56
+ if __name__ == "__main__":
57
+ main()
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import subprocess
4
4
 
5
- from exceptions import DockerComposeError
5
+ from devservices.exceptions import DockerComposeError
6
6
 
7
7
 
8
8
  def run_docker_compose_command(command: str) -> subprocess.CompletedProcess[str]:
@@ -3,12 +3,12 @@ from __future__ import annotations
3
3
  import os
4
4
  from dataclasses import dataclass
5
5
 
6
- from configs.service_config import ServiceConfig
7
- from exceptions import ConfigNotFoundError
8
- from exceptions import ConfigParseError
9
- from exceptions import ConfigValidationError
10
- from exceptions import ServiceNotFoundError
11
- from utils.devenv import get_coderoot
6
+ from devservices.configs.service_config import ServiceConfig
7
+ from devservices.exceptions import ConfigNotFoundError
8
+ from devservices.exceptions import ConfigParseError
9
+ from devservices.exceptions import ConfigValidationError
10
+ from devservices.exceptions import ServiceNotFoundError
11
+ from devservices.utils.devenv import get_coderoot
12
12
 
13
13
 
14
14
  @dataclass
@@ -20,7 +20,7 @@ class Service:
20
20
 
21
21
  def get_local_services(coderoot: str) -> list[Service]:
22
22
  """Get a list of services in the coderoot."""
23
- from configs.service_config import load_service_config_from_file
23
+ from devservices.configs.service_config import load_service_config_from_file
24
24
 
25
25
  services = []
26
26
  for repo in os.listdir(coderoot):
@@ -43,7 +43,7 @@ def get_local_services(coderoot: str) -> list[Service]:
43
43
  def find_matching_service(service_name: str | None = None) -> Service:
44
44
  """Find a service with the given name."""
45
45
  if service_name is None:
46
- from configs.service_config import load_service_config_from_file
46
+ from devservices.configs.service_config import load_service_config_from_file
47
47
 
48
48
  repo_path = os.getcwd()
49
49
  service_config = load_service_config_from_file(repo_path)
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: devservices
3
- Version: 0.0.1
4
- Requires-Python: >=3.12
3
+ Version: 0.0.2
4
+ Requires-Python: >=3.10
5
5
  Requires-Dist: pyyaml
6
6
  Requires-Dist: sentry-devenv
7
7
  Provides-Extra: dev
@@ -0,0 +1,32 @@
1
+ README.md
2
+ pyproject.toml
3
+ setup.cfg
4
+ devservices/__init__.py
5
+ devservices/constants.py
6
+ devservices/exceptions.py
7
+ devservices/main.py
8
+ devservices.egg-info/PKG-INFO
9
+ devservices.egg-info/SOURCES.txt
10
+ devservices.egg-info/dependency_links.txt
11
+ devservices.egg-info/entry_points.txt
12
+ devservices.egg-info/requires.txt
13
+ devservices.egg-info/top_level.txt
14
+ devservices/commands/__init__.py
15
+ devservices/commands/list_dependencies.py
16
+ devservices/commands/list_services.py
17
+ devservices/commands/logs.py
18
+ devservices/commands/start.py
19
+ devservices/commands/status.py
20
+ devservices/commands/stop.py
21
+ devservices/configs/service_config.py
22
+ devservices/utils/__init__.py
23
+ devservices/utils/console.py
24
+ devservices/utils/devenv.py
25
+ devservices/utils/docker_compose.py
26
+ devservices/utils/services.py
27
+ tests/__init__.py
28
+ tests/conftest.py
29
+ tests/testutils.py
30
+ tests/commands/test_start.py
31
+ tests/commands/test_stop.py
32
+ tests/configs/test_service_config.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ devservices = devservices.main:main
@@ -0,0 +1,4 @@
1
+ devservices
2
+ dist
3
+ scripts
4
+ tests
@@ -4,8 +4,9 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devservices"
7
- version = "0.0.1"
8
- requires-python = ">=3.12"
7
+ version = "0.0.2"
8
+ # 3.10 is just for internal pypi compat
9
+ requires-python = ">=3.10"
9
10
  dependencies = [
10
11
  "pyyaml",
11
12
  "sentry-devenv",
@@ -21,12 +22,11 @@ dev = [
21
22
  ]
22
23
 
23
24
  [project.scripts]
24
- devservices = "main:main"
25
+ devservices = "devservices.main:main"
26
+
27
+ [tool.setuptools.packages]
28
+ find = {}
25
29
 
26
- [tool.setuptools]
27
- package-dir = {"" = "src"}
28
- packages = {find = {where = ["src"]}}
29
- include-package-data = true
30
30
 
31
31
  [tool.mypy]
32
32
  python_version = "3.12"
File without changes
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import subprocess
5
+ from argparse import Namespace
6
+ from pathlib import Path
7
+ from unittest import mock
8
+
9
+ import pytest
10
+
11
+ from devservices.commands.start import start
12
+ from devservices.constants import DEVSERVICES_DIR_NAME
13
+ from devservices.constants import DOCKER_COMPOSE_FILE_NAME
14
+ from tests.testutils import create_config_file
15
+
16
+
17
+ @mock.patch("devservices.utils.docker_compose.subprocess.run")
18
+ def test_start_simple(mock_run: mock.Mock, tmp_path: Path) -> None:
19
+ config = {
20
+ "x-sentry-service-config": {
21
+ "version": 0.1,
22
+ "service_name": "example-service",
23
+ "dependencies": {
24
+ "redis": {"description": "Redis"},
25
+ "clickhouse": {"description": "Clickhouse"},
26
+ },
27
+ "modes": {"default": ["redis", "clickhouse"]},
28
+ },
29
+ "services": {
30
+ "redis": {"image": "redis:6.2.14-alpine"},
31
+ "clickhouse": {
32
+ "image": "altinity/clickhouse-server:23.8.11.29.altinitystable"
33
+ },
34
+ },
35
+ }
36
+ create_config_file(tmp_path, config)
37
+ os.chdir(tmp_path)
38
+
39
+ args = Namespace(service_name=None)
40
+ start(args)
41
+
42
+ mock_run.assert_called_once_with(
43
+ [
44
+ "docker",
45
+ "compose",
46
+ "-f",
47
+ f"{tmp_path}/{DEVSERVICES_DIR_NAME}/{DOCKER_COMPOSE_FILE_NAME}",
48
+ "up",
49
+ "-d",
50
+ "redis",
51
+ "clickhouse",
52
+ ],
53
+ check=True,
54
+ capture_output=True,
55
+ text=True,
56
+ )
57
+
58
+
59
+ @mock.patch("devservices.utils.docker_compose.subprocess.run")
60
+ def test_start_error(
61
+ mock_run: mock.Mock, capsys: pytest.CaptureFixture[str], tmp_path: Path
62
+ ) -> None:
63
+ mock_run.side_effect = subprocess.CalledProcessError(
64
+ returncode=1, stderr="Docker Compose error", cmd=""
65
+ )
66
+ config = {
67
+ "x-sentry-service-config": {
68
+ "version": 0.1,
69
+ "service_name": "example-service",
70
+ "dependencies": {
71
+ "redis": {"description": "Redis"},
72
+ "clickhouse": {"description": "Clickhouse"},
73
+ },
74
+ "modes": {"default": ["redis", "clickhouse"]},
75
+ },
76
+ "services": {
77
+ "redis": {"image": "redis:6.2.14-alpine"},
78
+ "clickhouse": {
79
+ "image": "altinity/clickhouse-server:23.8.11.29.altinitystable"
80
+ },
81
+ },
82
+ }
83
+
84
+ create_config_file(tmp_path, config)
85
+ os.chdir(tmp_path)
86
+
87
+ args = Namespace(service_name=None)
88
+
89
+ with pytest.raises(SystemExit):
90
+ start(args)
91
+
92
+ # Capture the printed output
93
+ captured = capsys.readouterr()
94
+
95
+ assert (
96
+ "Failed to start example-service: Docker Compose error" in captured.out.strip()
97
+ )
@@ -0,0 +1,97 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import subprocess
5
+ from argparse import Namespace
6
+ from pathlib import Path
7
+ from unittest import mock
8
+
9
+ import pytest
10
+
11
+ from devservices.commands.stop import stop
12
+ from devservices.constants import DEVSERVICES_DIR_NAME
13
+ from devservices.constants import DOCKER_COMPOSE_FILE_NAME
14
+ from tests.testutils import create_config_file
15
+
16
+
17
+ @mock.patch("devservices.utils.docker_compose.subprocess.run")
18
+ def test_stop_simple(mock_run: mock.Mock, tmp_path: Path) -> None:
19
+ config = {
20
+ "x-sentry-service-config": {
21
+ "version": 0.1,
22
+ "service_name": "example-service",
23
+ "dependencies": {
24
+ "redis": {"description": "Redis"},
25
+ "clickhouse": {"description": "Clickhouse"},
26
+ },
27
+ "modes": {"default": ["redis", "clickhouse"]},
28
+ },
29
+ "services": {
30
+ "redis": {"image": "redis:6.2.14-alpine"},
31
+ "clickhouse": {
32
+ "image": "altinity/clickhouse-server:23.8.11.29.altinitystable"
33
+ },
34
+ },
35
+ }
36
+ create_config_file(tmp_path, config)
37
+ os.chdir(tmp_path)
38
+
39
+ args = Namespace(service_name=None)
40
+
41
+ stop(args)
42
+
43
+ mock_run.assert_called_once_with(
44
+ [
45
+ "docker",
46
+ "compose",
47
+ "-f",
48
+ f"{tmp_path}/{DEVSERVICES_DIR_NAME}/{DOCKER_COMPOSE_FILE_NAME}",
49
+ "down",
50
+ "redis",
51
+ "clickhouse",
52
+ ],
53
+ check=True,
54
+ capture_output=True,
55
+ text=True,
56
+ )
57
+
58
+
59
+ @mock.patch("devservices.utils.docker_compose.subprocess.run")
60
+ def test_stop_error(
61
+ mock_run: mock.Mock, capsys: pytest.CaptureFixture[str], tmp_path: Path
62
+ ) -> None:
63
+ mock_run.side_effect = subprocess.CalledProcessError(
64
+ returncode=1, stderr="Docker Compose error", cmd=""
65
+ )
66
+ config = {
67
+ "x-sentry-service-config": {
68
+ "version": 0.1,
69
+ "service_name": "example-service",
70
+ "dependencies": {
71
+ "redis": {"description": "Redis"},
72
+ "clickhouse": {"description": "Clickhouse"},
73
+ },
74
+ "modes": {"default": ["redis", "clickhouse"]},
75
+ },
76
+ "services": {
77
+ "redis": {"image": "redis:6.2.14-alpine"},
78
+ "clickhouse": {
79
+ "image": "altinity/clickhouse-server:23.8.11.29.altinitystable"
80
+ },
81
+ },
82
+ }
83
+
84
+ create_config_file(tmp_path, config)
85
+ os.chdir(tmp_path)
86
+
87
+ args = Namespace(service_name=None)
88
+
89
+ with pytest.raises(SystemExit):
90
+ stop(args)
91
+
92
+ # Capture the printed output
93
+ captured = capsys.readouterr()
94
+
95
+ assert (
96
+ "Failed to stop example-service: Docker Compose error" in captured.out.strip()
97
+ )
@@ -0,0 +1,323 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+
8
+ from devservices.configs.service_config import load_service_config_from_file
9
+ from devservices.exceptions import ConfigNotFoundError
10
+ from devservices.exceptions import ConfigParseError
11
+ from devservices.exceptions import ConfigValidationError
12
+ from tests.testutils import create_config_file
13
+
14
+
15
+ @pytest.mark.parametrize(
16
+ "service_name, dependencies, modes",
17
+ [
18
+ (
19
+ "example-service",
20
+ {"example-dependency": {"description": "Example dependency"}},
21
+ {"default": ["example-dependency"]},
22
+ ),
23
+ (
24
+ "example-service",
25
+ {
26
+ "example-dependency-1": {
27
+ "description": "Example dependency 1",
28
+ "link": "https://example.com",
29
+ },
30
+ "example-dependency-2": {
31
+ "description": "Example dependency 2",
32
+ },
33
+ },
34
+ {"default": ["example-dependency-1", "example-dependency-2"]},
35
+ ),
36
+ (
37
+ "example-service",
38
+ {
39
+ "example-dependency-1": {
40
+ "description": "Example dependency 1",
41
+ "link": "https://example.com",
42
+ },
43
+ "example-dependency-2": {
44
+ "description": "Example dependency 2",
45
+ },
46
+ },
47
+ {"default": ["example-dependency-1"], "custom": ["example-dependency-2"]},
48
+ ),
49
+ ],
50
+ )
51
+ def test_load_service_config_from_file(
52
+ tmp_path: Path,
53
+ service_name: str,
54
+ dependencies: dict[str, dict[str, object]],
55
+ modes: dict[str, list[str]],
56
+ ) -> None:
57
+ config = {
58
+ "x-sentry-service-config": {
59
+ "version": 0.1,
60
+ "service_name": service_name,
61
+ "dependencies": {key: value for key, value in dependencies.items()},
62
+ "modes": {key: value for key, value in modes.items()},
63
+ }
64
+ }
65
+ create_config_file(tmp_path, config)
66
+
67
+ service_config = load_service_config_from_file(str(tmp_path))
68
+ assert asdict(service_config) == {
69
+ "version": 0.1,
70
+ "service_name": service_name,
71
+ "dependencies": {
72
+ key: {"description": value["description"], "link": value.get("link")}
73
+ for key, value in dependencies.items()
74
+ },
75
+ "modes": modes,
76
+ }
77
+
78
+
79
+ def test_load_service_config_from_file_no_dependencies(tmp_path: Path) -> None:
80
+ config = {
81
+ "x-sentry-service-config": {
82
+ "version": 0.1,
83
+ "service_name": "example-service",
84
+ "modes": {"default": []},
85
+ }
86
+ }
87
+ create_config_file(tmp_path, config)
88
+
89
+ service_config = load_service_config_from_file(str(tmp_path))
90
+ assert asdict(service_config) == {
91
+ "version": 0.1,
92
+ "service_name": "example-service",
93
+ "dependencies": {},
94
+ "modes": {"default": []},
95
+ }
96
+
97
+
98
+ def test_load_service_config_from_file_missing_config(tmp_path: Path) -> None:
99
+ with pytest.raises(ConfigNotFoundError) as e:
100
+ load_service_config_from_file(str(tmp_path))
101
+ assert (
102
+ str(e.value)
103
+ == f"Config file not found in directory: {tmp_path / 'devservices' / 'docker-compose.yml'}"
104
+ )
105
+
106
+
107
+ def test_load_service_config_from_file_invalid_version(tmp_path: Path) -> None:
108
+ config = {
109
+ "x-sentry-service-config": {
110
+ "version": 0.2,
111
+ "service_name": "example-service",
112
+ "dependencies": {
113
+ "example-dependency": {"description": "Example dependency"}
114
+ },
115
+ "modes": {"default": ["example-dependency"]},
116
+ }
117
+ }
118
+ create_config_file(tmp_path, config)
119
+
120
+ with pytest.raises(ConfigValidationError) as e:
121
+ load_service_config_from_file(str(tmp_path))
122
+ assert str(e.value) == "Invalid version '0.2' in service config"
123
+
124
+
125
+ def test_load_service_config_from_file_missing_version(tmp_path: Path) -> None:
126
+ config = {
127
+ "x-sentry-service-config": {
128
+ "dependencies": {
129
+ "example-dependency": {"description": "Example dependency"}
130
+ },
131
+ "modes": {"default": ["example-dependency"]},
132
+ }
133
+ }
134
+ create_config_file(tmp_path, config)
135
+
136
+ with pytest.raises(ConfigValidationError) as e:
137
+ load_service_config_from_file(str(tmp_path))
138
+ assert str(e.value) == "Version is required in service config"
139
+
140
+
141
+ def test_load_service_config_from_file_missing_service_name(tmp_path: Path) -> None:
142
+ config = {
143
+ "x-sentry-service-config": {
144
+ "version": 0.1,
145
+ "dependencies": {
146
+ "example-dependency": {"description": "Example dependency"}
147
+ },
148
+ "modes": {"default": ["example-dependency"]},
149
+ }
150
+ }
151
+ create_config_file(tmp_path, config)
152
+
153
+ with pytest.raises(ConfigValidationError) as e:
154
+ load_service_config_from_file(str(tmp_path))
155
+ assert str(e.value) == "Service name is required in service config"
156
+
157
+
158
+ def test_load_service_config_from_file_invalid_dependency(tmp_path: Path) -> None:
159
+ config = {
160
+ "x-sentry-service-config": {
161
+ "version": 0.1,
162
+ "service_name": "example-service",
163
+ "dependencies": {
164
+ "example-dependency": {"description": "Example dependency"}
165
+ },
166
+ "modes": {"default": ["example-dependency", "unknown-dependency"]},
167
+ }
168
+ }
169
+ create_config_file(tmp_path, config)
170
+
171
+ with pytest.raises(ConfigValidationError) as e:
172
+ load_service_config_from_file(str(tmp_path))
173
+ assert (
174
+ str(e.value)
175
+ == "Service 'unknown-dependency' in mode 'default' is not defined in dependencies"
176
+ )
177
+
178
+
179
+ def test_load_service_config_from_file_missing_default_mode(tmp_path: Path) -> None:
180
+ config = {
181
+ "x-sentry-service-config": {
182
+ "version": 0.1,
183
+ "service_name": "example-service",
184
+ "dependencies": {
185
+ "example-dependency": {"description": "Example dependency"}
186
+ },
187
+ "modes": {"custom": ["example-dependency"]},
188
+ }
189
+ }
190
+ create_config_file(tmp_path, config)
191
+
192
+ with pytest.raises(ConfigValidationError) as e:
193
+ load_service_config_from_file(str(tmp_path))
194
+ assert str(e.value) == "Default mode is required in service config"
195
+
196
+
197
+ def test_load_service_config_from_file_no_modes(tmp_path: Path) -> None:
198
+ config = {
199
+ "x-sentry-service-config": {
200
+ "version": 0.1,
201
+ "service_name": "example-service",
202
+ "dependencies": {
203
+ "example-dependency": {"description": "Example dependency"}
204
+ },
205
+ }
206
+ }
207
+ create_config_file(tmp_path, config)
208
+
209
+ with pytest.raises(ConfigValidationError) as e:
210
+ load_service_config_from_file(str(tmp_path))
211
+ assert str(e.value) == "Default mode is required in service config"
212
+
213
+
214
+ def test_load_service_config_from_file_invalid_dependencies(tmp_path: Path) -> None:
215
+ config = {
216
+ "x-sentry-service-config": {
217
+ "version": 0.1,
218
+ "service_name": "example-service",
219
+ "dependencies": {
220
+ "example-dependency": {
221
+ "description": "Example dependency",
222
+ "unknown": "key",
223
+ }
224
+ },
225
+ "modes": {"default": ["example-dependency"]},
226
+ }
227
+ }
228
+ create_config_file(tmp_path, config)
229
+
230
+ with pytest.raises(ConfigParseError) as e:
231
+ load_service_config_from_file(str(tmp_path))
232
+ assert (
233
+ str(e.value)
234
+ == "Error parsing service dependencies: Dependency.__init__() got an unexpected keyword argument 'unknown'"
235
+ )
236
+
237
+
238
+ def test_load_service_config_from_file_invalid_modes(tmp_path: Path) -> None:
239
+ config = {
240
+ "x-sentry-service-config": {
241
+ "version": 0.1,
242
+ "service_name": "example-service",
243
+ "dependencies": {
244
+ "example-dependency": {"description": "Example dependency"}
245
+ },
246
+ "modes": {
247
+ "default": ["example-dependency"],
248
+ "custom": "example-dependency",
249
+ },
250
+ }
251
+ }
252
+ create_config_file(tmp_path, config)
253
+
254
+ with pytest.raises(ConfigValidationError) as e:
255
+ load_service_config_from_file(str(tmp_path))
256
+ assert str(e.value) == "Services in mode 'custom' must be a list"
257
+
258
+
259
+ def test_load_service_config_from_file_no_x_sentry_service_config(
260
+ tmp_path: Path,
261
+ ) -> None:
262
+ config = {
263
+ "x-not-sentry-service-config": {
264
+ "version": 0.1,
265
+ "service_name": "example-service",
266
+ "dependencies": {
267
+ "example-dependency": {"description": "Example dependency"}
268
+ },
269
+ "modes": {"default": ["example-dependency"]},
270
+ }
271
+ }
272
+ create_config_file(tmp_path, config)
273
+
274
+ with pytest.raises(ConfigParseError) as e:
275
+ load_service_config_from_file(str(tmp_path))
276
+ assert str(e.value) == "Config file does not contain 'x-sentry-service-config' key"
277
+
278
+
279
+ def test_load_service_config_from_file_invalid_yaml(tmp_path: Path) -> None:
280
+ config = """x-sentry-service-config
281
+ version: 0.1
282
+ service_name: "example-service"
283
+ dependencies:
284
+ example-dependency:
285
+ description: "Example dependency"
286
+ modes:
287
+ default: ["example-dependency"]"""
288
+ devservices_dir = Path(tmp_path, "devservices")
289
+ devservices_dir.mkdir(parents=True, exist_ok=True)
290
+ tmp_file = Path(devservices_dir, "docker-compose.yml")
291
+ with tmp_file.open("w") as f:
292
+ f.write(config)
293
+
294
+ with pytest.raises(ConfigParseError) as e:
295
+ load_service_config_from_file(str(tmp_path))
296
+ assert (
297
+ str(e.value)
298
+ == f"Error parsing config file: mapping values are not allowed here\n in \"{tmp_path / 'devservices' / 'docker-compose.yml'}\", line 2, column 12"
299
+ )
300
+
301
+
302
+ def test_load_service_config_from_file_invalid_yaml_tag(tmp_path: Path) -> None:
303
+ config = """x-sentry-service-config:
304
+ version: 0.1
305
+ service_name: "example-service"
306
+ dependencies:
307
+ example-dependency:
308
+ description: "Example dependency"
309
+ link: !!invalid_tag "https://example.com"
310
+ modes:
311
+ default: ["example-dependency"]"""
312
+ devservices_dir = Path(tmp_path, "devservices")
313
+ devservices_dir.mkdir(parents=True, exist_ok=True)
314
+ tmp_file = Path(devservices_dir, "docker-compose.yml")
315
+ with tmp_file.open("w") as f:
316
+ f.write(config)
317
+
318
+ with pytest.raises(ConfigParseError) as e:
319
+ load_service_config_from_file(str(tmp_path))
320
+ assert (
321
+ str(e.value)
322
+ == f"Error parsing config file: could not determine a constructor for the tag 'tag:yaml.org,2002:invalid_tag'\n in \"{tmp_path / 'devservices' / 'docker-compose.yml'}\", line 7, column 19"
323
+ )
File without changes
@@ -1,23 +0,0 @@
1
- README.md
2
- pyproject.toml
3
- setup.cfg
4
- src/commands/__init__.py
5
- src/commands/list_dependencies.py
6
- src/commands/list_services.py
7
- src/commands/logs.py
8
- src/commands/start.py
9
- src/commands/status.py
10
- src/commands/stop.py
11
- src/configs/service_config.py
12
- src/devservices.egg-info/PKG-INFO
13
- src/devservices.egg-info/SOURCES.txt
14
- src/devservices.egg-info/dependency_links.txt
15
- src/devservices.egg-info/entry_points.txt
16
- src/devservices.egg-info/requires.txt
17
- src/devservices.egg-info/top_level.txt
18
- src/utils/__init__.py
19
- src/utils/console.py
20
- src/utils/devenv.py
21
- src/utils/docker_compose.py
22
- src/utils/services.py
23
- tests/testutils.py
@@ -1,2 +0,0 @@
1
- [console_scripts]
2
- devservices = main:main
@@ -1,3 +0,0 @@
1
- commands
2
- configs
3
- utils
File without changes
File without changes