devservices 1.3.0__tar.gz → 1.3.1__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 (68) hide show
  1. {devservices-1.3.0 → devservices-1.3.1}/PKG-INFO +1 -1
  2. {devservices-1.3.0 → devservices-1.3.1}/README.md +3 -2
  3. {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/up.py +3 -1
  4. {devservices-1.3.0 → devservices-1.3.1}/devservices/configs/service_config.py +13 -0
  5. {devservices-1.3.0 → devservices-1.3.1}/devservices/constants.py +2 -2
  6. {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/docker.py +14 -5
  7. {devservices-1.3.0 → devservices-1.3.1}/devservices.egg-info/PKG-INFO +1 -1
  8. {devservices-1.3.0 → devservices-1.3.1}/pyproject.toml +5 -3
  9. {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_up.py +2 -1
  10. {devservices-1.3.0 → devservices-1.3.1}/tests/configs/test_service_config.py +41 -0
  11. {devservices-1.3.0 → devservices-1.3.1}/tests/utils/test_docker.py +4 -0
  12. {devservices-1.3.0 → devservices-1.3.1}/LICENSE.md +0 -0
  13. {devservices-1.3.0 → devservices-1.3.1}/devservices/__init__.py +0 -0
  14. {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/__init__.py +0 -0
  15. {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/down.py +0 -0
  16. {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/foreground.py +0 -0
  17. {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/list_dependencies.py +0 -0
  18. {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/list_services.py +0 -0
  19. {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/logs.py +0 -0
  20. {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/purge.py +0 -0
  21. {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/reset.py +0 -0
  22. {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/serve.py +0 -0
  23. {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/status.py +0 -0
  24. {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/toggle.py +0 -0
  25. {devservices-1.3.0 → devservices-1.3.1}/devservices/commands/update.py +0 -0
  26. {devservices-1.3.0 → devservices-1.3.1}/devservices/exceptions.py +0 -0
  27. {devservices-1.3.0 → devservices-1.3.1}/devservices/main.py +0 -0
  28. {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/__init__.py +0 -0
  29. {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/check_for_update.py +0 -0
  30. {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/console.py +0 -0
  31. {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/dependencies.py +0 -0
  32. {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/devenv.py +0 -0
  33. {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/docker_compose.py +0 -0
  34. {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/file_lock.py +0 -0
  35. {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/git.py +0 -0
  36. {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/install_binary.py +0 -0
  37. {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/services.py +0 -0
  38. {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/state.py +0 -0
  39. {devservices-1.3.0 → devservices-1.3.1}/devservices/utils/supervisor.py +0 -0
  40. {devservices-1.3.0 → devservices-1.3.1}/devservices.egg-info/SOURCES.txt +0 -0
  41. {devservices-1.3.0 → devservices-1.3.1}/devservices.egg-info/dependency_links.txt +0 -0
  42. {devservices-1.3.0 → devservices-1.3.1}/devservices.egg-info/entry_points.txt +0 -0
  43. {devservices-1.3.0 → devservices-1.3.1}/devservices.egg-info/requires.txt +0 -0
  44. {devservices-1.3.0 → devservices-1.3.1}/devservices.egg-info/top_level.txt +0 -0
  45. {devservices-1.3.0 → devservices-1.3.1}/setup.cfg +0 -0
  46. {devservices-1.3.0 → devservices-1.3.1}/testing/__init__.py +0 -0
  47. {devservices-1.3.0 → devservices-1.3.1}/testing/utils.py +0 -0
  48. {devservices-1.3.0 → devservices-1.3.1}/tests/__init__.py +0 -0
  49. {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_down.py +0 -0
  50. {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_foreground.py +0 -0
  51. {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_list_dependencies.py +0 -0
  52. {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_list_services.py +0 -0
  53. {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_logs.py +0 -0
  54. {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_purge.py +0 -0
  55. {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_reset.py +0 -0
  56. {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_serve.py +0 -0
  57. {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_status.py +0 -0
  58. {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_toggle.py +0 -0
  59. {devservices-1.3.0 → devservices-1.3.1}/tests/commands/test_update.py +0 -0
  60. {devservices-1.3.0 → devservices-1.3.1}/tests/conftest.py +0 -0
  61. {devservices-1.3.0 → devservices-1.3.1}/tests/utils/test_check_for_update.py +0 -0
  62. {devservices-1.3.0 → devservices-1.3.1}/tests/utils/test_dependencies.py +0 -0
  63. {devservices-1.3.0 → devservices-1.3.1}/tests/utils/test_docker_compose.py +0 -0
  64. {devservices-1.3.0 → devservices-1.3.1}/tests/utils/test_git.py +0 -0
  65. {devservices-1.3.0 → devservices-1.3.1}/tests/utils/test_install_binary.py +0 -0
  66. {devservices-1.3.0 → devservices-1.3.1}/tests/utils/test_services.py +0 -0
  67. {devservices-1.3.0 → devservices-1.3.1}/tests/utils/test_state.py +0 -0
  68. {devservices-1.3.0 → devservices-1.3.1}/tests/utils/test_supervisor.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devservices
3
- Version: 1.3.0
3
+ Version: 1.3.1
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.3.0
36
+ devservices==1.3.1
37
37
  ```
38
38
 
39
39
  ### 2. Add devservices config files
@@ -50,6 +50,7 @@ The configuration file is a yaml file that looks like this:
50
50
  # - local: A dependency that is defined in the config file. These dependencies do not have a remote field and must correspond to either a service defined in the 'services' section or a program defined in the 'x-programs' section.
51
51
  # - remote: A dependency that is defined in the devservices directory in a remote repository. These configs are automatically fetched from the remote repository and installed. Any dependency with a remote field will be treated as a remote dependency. Example: https://github.com/getsentry/snuba/blob/59a5258ccbb502827ebc1d3b1bf80c607a3301bf/devservices/config.yml#L8
52
52
  # - modes: A list of modes for the service. Each mode includes a list of dependencies that are used in that mode.
53
+ # - healthcheck_timeout: Optional. The number of seconds to wait for all containers to become healthy before failing. Defaults to 180.
53
54
  x-sentry-service-config:
54
55
  version: 0.1
55
56
  service_name: example-service
@@ -173,5 +174,5 @@ networks:
173
174
  ```sh
174
175
  uv sync
175
176
  direnv allow
176
- pytest
177
+ pytest -n4
177
178
  ```
@@ -390,7 +390,9 @@ def bring_up_docker_compose_services(
390
390
  )
391
391
  exit(1)
392
392
  try:
393
- check_all_containers_healthy(status, containers_to_check)
393
+ check_all_containers_healthy(
394
+ status, containers_to_check, timeout=service.config.healthcheck_timeout
395
+ )
394
396
  except ContainerHealthcheckFailedError as e:
395
397
  status.failure(str(e))
396
398
  exit(1)
@@ -9,6 +9,7 @@ from supervisor.options import ServerOptions
9
9
 
10
10
  from devservices.constants import CONFIG_FILE_NAME
11
11
  from devservices.constants import DEVSERVICES_DIR_NAME
12
+ from devservices.constants import HEALTHCHECK_TIMEOUT
12
13
  from devservices.constants import DependencyType
13
14
  from devservices.exceptions import ConfigNotFoundError
14
15
  from devservices.exceptions import ConfigParseError
@@ -40,6 +41,7 @@ class ServiceConfig:
40
41
  service_name: str
41
42
  dependencies: dict[str, Dependency]
42
43
  modes: dict[str, list[str]]
44
+ healthcheck_timeout: int = HEALTHCHECK_TIMEOUT
43
45
 
44
46
  def __post_init__(self) -> None:
45
47
  self._validate()
@@ -59,6 +61,14 @@ class ServiceConfig:
59
61
  if "default" not in self.modes:
60
62
  raise ConfigValidationError("Default mode is required in service config")
61
63
 
64
+ if isinstance(self.healthcheck_timeout, bool) or (
65
+ not isinstance(self.healthcheck_timeout, int)
66
+ or self.healthcheck_timeout <= 0
67
+ ):
68
+ raise ConfigValidationError(
69
+ "healthcheck_timeout must be a positive integer"
70
+ )
71
+
62
72
  for mode, services in self.modes.items():
63
73
  if not isinstance(services, list):
64
74
  raise ConfigValidationError(f"Services in mode '{mode}' must be a list")
@@ -142,6 +152,9 @@ def load_service_config_from_file(
142
152
  service_name=service_config_data.get("service_name"),
143
153
  dependencies=dependencies,
144
154
  modes=service_config_data.get("modes", {}),
155
+ healthcheck_timeout=service_config_data.get("healthcheck_timeout")
156
+ if service_config_data.get("healthcheck_timeout") is not None
157
+ else HEALTHCHECK_TIMEOUT,
145
158
  )
146
159
 
147
160
  return service_config
@@ -65,7 +65,7 @@ DEVSERVICES_LATEST_VERSION_CACHE_FILE = os.path.join(
65
65
  DEVSERVICES_CACHE_DIR, "latest_version.txt"
66
66
  )
67
67
  DEVSERVICES_LATEST_VERSION_CACHE_TTL = timedelta(minutes=15)
68
- # Healthcheck timeout set to 2 minutes to account for slow healthchecks
69
- HEALTHCHECK_TIMEOUT = 120
68
+ # Healthcheck timeout set to 3 minutes to account for slow healthchecks
69
+ HEALTHCHECK_TIMEOUT = 180
70
70
  HEALTHCHECK_INTERVAL = 5
71
71
  SUPERVISOR_TIMEOUT = 10
@@ -43,25 +43,34 @@ def check_docker_daemon_running() -> None:
43
43
 
44
44
 
45
45
  def check_all_containers_healthy(
46
- status: Status, containers: list[ContainerNames]
46
+ status: Status,
47
+ containers: list[ContainerNames],
48
+ timeout: int = HEALTHCHECK_TIMEOUT,
47
49
  ) -> None:
48
50
  """Ensures all containers are healthy."""
49
51
  status.info("Waiting for all containers to be healthy")
50
52
  with concurrent.futures.ThreadPoolExecutor() as healthcheck_executor:
51
53
  futures = [
52
- healthcheck_executor.submit(wait_for_healthy, container, status)
54
+ healthcheck_executor.submit(wait_for_healthy, container, status, timeout)
53
55
  for container in containers
54
56
  ]
55
57
  for future in concurrent.futures.as_completed(futures):
56
58
  future.result()
57
59
 
58
60
 
59
- def wait_for_healthy(container: ContainerNames, status: Status) -> None:
61
+ def wait_for_healthy(
62
+ container: ContainerNames,
63
+ status: Status,
64
+ timeout: int = HEALTHCHECK_TIMEOUT,
65
+ ) -> None:
60
66
  """
61
67
  Polls a Docker container's health status until it becomes healthy or a timeout is reached.
62
68
  """
69
+ status.info(
70
+ f"Waiting for {container.short_name} to be healthy (timeout: {timeout}s)"
71
+ )
63
72
  start = time.time()
64
- while time.time() - start < HEALTHCHECK_TIMEOUT:
73
+ while time.time() - start < timeout:
65
74
  # Run docker inspect to get the container's health status
66
75
  try:
67
76
  # For containers with no healthchecks, the output will be "unknown"
@@ -96,7 +105,7 @@ def wait_for_healthy(container: ContainerNames, status: Status) -> None:
96
105
  # If not healthy, wait and try again
97
106
  time.sleep(HEALTHCHECK_INTERVAL)
98
107
 
99
- raise ContainerHealthcheckFailedError(container.short_name, HEALTHCHECK_TIMEOUT)
108
+ raise ContainerHealthcheckFailedError(container.short_name, timeout)
100
109
 
101
110
 
102
111
  @dataclass
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devservices
3
- Version: 1.3.0
3
+ Version: 1.3.1
4
4
  Requires-Python: >=3.11
5
5
  License-File: LICENSE.md
6
6
  Requires-Dist: pyyaml
@@ -4,8 +4,9 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devservices"
7
- version = "1.3.0"
7
+ version = "1.3.1"
8
8
  # 3.11 is just for internal pypi compat
9
+ # but we test/support on 3.13
9
10
  requires-python = ">=3.11"
10
11
  dependencies = [
11
12
  "pyyaml",
@@ -21,13 +22,14 @@ dev = [
21
22
  "freezegun",
22
23
  "mypy",
23
24
  "pre-commit",
24
- "pytest",
25
+ "pytest>9",
25
26
  "pytest-cov",
26
27
  "ruff",
27
28
  "setuptools>=70",
28
29
  "shellcheck-py",
29
30
  "wheel",
30
31
  "types-PyYAML",
32
+ "pytest-xdist>=3.5.0",
31
33
  ]
32
34
 
33
35
  [project.scripts]
@@ -40,7 +42,7 @@ find = {}
40
42
  url = "https://pypi.devinfra.sentry.io/simple"
41
43
 
42
44
  [tool.mypy]
43
- python_version = "3.12"
45
+ python_version = "3.13"
44
46
  strict = true
45
47
  ignore_missing_imports = true
46
48
 
@@ -865,7 +865,7 @@ def test_up_docker_compose_container_healthcheck_failed(
865
865
  assert "Starting clickhouse" in captured.out.strip()
866
866
  assert "Starting redis" in captured.out.strip()
867
867
  assert (
868
- "Container container1 did not become healthy within 120 seconds."
868
+ "Container container1 did not become healthy within 180 seconds."
869
869
  in captured.out.strip()
870
870
  )
871
871
 
@@ -1376,6 +1376,7 @@ def test_up_multiple_modes_overlapping_running_service(
1376
1376
  mock_check_all_containers_healthy.assert_called_once_with(
1377
1377
  mock.ANY,
1378
1378
  ["container1", "container2"],
1379
+ timeout=mock.ANY,
1379
1380
  )
1380
1381
 
1381
1382
  captured = capsys.readouterr()
@@ -111,6 +111,7 @@ def test_load_service_config_from_file(
111
111
  for key, value in dependencies.items()
112
112
  },
113
113
  "modes": modes,
114
+ "healthcheck_timeout": 180,
114
115
  }
115
116
 
116
117
 
@@ -131,9 +132,49 @@ def test_load_service_config_from_file_no_dependencies(tmp_path: Path) -> None:
131
132
  "service_name": "example-service",
132
133
  "dependencies": {},
133
134
  "modes": {"default": []},
135
+ "healthcheck_timeout": 180,
134
136
  }
135
137
 
136
138
 
139
+ def test_load_service_config_from_file_null_healthcheck_timeout(
140
+ tmp_path: Path,
141
+ ) -> None:
142
+ config = {
143
+ "x-sentry-service-config": {
144
+ "version": 0.1,
145
+ "service_name": "example-service",
146
+ "modes": {"default": []},
147
+ "healthcheck_timeout": None,
148
+ },
149
+ "services": {},
150
+ }
151
+ create_config_file(tmp_path, config)
152
+
153
+ service_config = load_service_config_from_file(str(tmp_path))
154
+ assert service_config.healthcheck_timeout == 180
155
+
156
+
157
+ @pytest.mark.parametrize("invalid_timeout", [-1, 0, "120", True, False])
158
+ def test_load_service_config_from_file_invalid_healthcheck_timeout(
159
+ tmp_path: Path,
160
+ invalid_timeout: object,
161
+ ) -> None:
162
+ config = {
163
+ "x-sentry-service-config": {
164
+ "version": 0.1,
165
+ "service_name": "example-service",
166
+ "modes": {"default": []},
167
+ "healthcheck_timeout": invalid_timeout,
168
+ },
169
+ "services": {},
170
+ }
171
+ create_config_file(tmp_path, config)
172
+
173
+ with pytest.raises(ConfigValidationError) as e:
174
+ load_service_config_from_file(str(tmp_path))
175
+ assert str(e.value) == "healthcheck_timeout must be a positive integer"
176
+
177
+
137
178
  def test_load_service_config_from_file_missing_config(tmp_path: Path) -> None:
138
179
  with pytest.raises(ConfigNotFoundError) as e:
139
180
  load_service_config_from_file(str(tmp_path))
@@ -533,10 +533,12 @@ def test_check_all_containers_healthy_success(
533
533
  mock.call(
534
534
  ContainerNames(name="devservices-container1", short_name="container1"),
535
535
  mock_status,
536
+ 180,
536
537
  ),
537
538
  mock.call(
538
539
  ContainerNames(name="devservices-container2", short_name="container2"),
539
540
  mock_status,
541
+ 180,
540
542
  ),
541
543
  ]
542
544
  )
@@ -671,10 +673,12 @@ def test_check_all_containers_healthy_failure(
671
673
  mock.call(
672
674
  ContainerNames(name="devservices-container1", short_name="container1"),
673
675
  mock_status,
676
+ 180,
674
677
  ),
675
678
  mock.call(
676
679
  ContainerNames(name="devservices-container2", short_name="container2"),
677
680
  mock_status,
681
+ 180,
678
682
  ),
679
683
  ]
680
684
  )
File without changes
File without changes