devservices 1.0.8__tar.gz → 1.0.9__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 (57) hide show
  1. {devservices-1.0.8 → devservices-1.0.9}/PKG-INFO +2 -2
  2. {devservices-1.0.8 → devservices-1.0.9}/README.md +1 -1
  3. {devservices-1.0.8 → devservices-1.0.9}/devservices/commands/down.py +41 -8
  4. {devservices-1.0.8 → devservices-1.0.9}/devservices/commands/list_dependencies.py +7 -0
  5. {devservices-1.0.8 → devservices-1.0.9}/devservices/commands/logs.py +7 -0
  6. {devservices-1.0.8 → devservices-1.0.9}/devservices/commands/status.py +7 -0
  7. {devservices-1.0.8 → devservices-1.0.9}/devservices/commands/up.py +7 -0
  8. {devservices-1.0.8 → devservices-1.0.9}/devservices/configs/service_config.py +3 -1
  9. {devservices-1.0.8 → devservices-1.0.9}/devservices/utils/console.py +3 -0
  10. {devservices-1.0.8 → devservices-1.0.9}/devservices/utils/dependencies.py +40 -7
  11. {devservices-1.0.8 → devservices-1.0.9}/devservices/utils/docker_compose.py +28 -18
  12. {devservices-1.0.8 → devservices-1.0.9}/devservices/utils/services.py +8 -1
  13. {devservices-1.0.8 → devservices-1.0.9}/devservices.egg-info/PKG-INFO +2 -2
  14. {devservices-1.0.8 → devservices-1.0.9}/pyproject.toml +1 -1
  15. devservices-1.0.9/tests/commands/test_down.py +861 -0
  16. {devservices-1.0.8 → devservices-1.0.9}/tests/commands/test_list_dependencies.py +21 -0
  17. {devservices-1.0.8 → devservices-1.0.9}/tests/commands/test_logs.py +22 -1
  18. {devservices-1.0.8 → devservices-1.0.9}/tests/commands/test_status.py +21 -0
  19. {devservices-1.0.8 → devservices-1.0.9}/tests/commands/test_up.py +170 -14
  20. {devservices-1.0.8 → devservices-1.0.9}/tests/configs/test_service_config.py +1 -1
  21. {devservices-1.0.8 → devservices-1.0.9}/tests/utils/test_docker_compose.py +3 -9
  22. {devservices-1.0.8 → devservices-1.0.9}/tests/utils/test_services.py +65 -2
  23. devservices-1.0.8/tests/commands/test_down.py +0 -265
  24. {devservices-1.0.8 → devservices-1.0.9}/LICENSE.md +0 -0
  25. {devservices-1.0.8 → devservices-1.0.9}/devservices/__init__.py +0 -0
  26. {devservices-1.0.8 → devservices-1.0.9}/devservices/commands/__init__.py +0 -0
  27. {devservices-1.0.8 → devservices-1.0.9}/devservices/commands/list_services.py +0 -0
  28. {devservices-1.0.8 → devservices-1.0.9}/devservices/commands/purge.py +0 -0
  29. {devservices-1.0.8 → devservices-1.0.9}/devservices/commands/update.py +0 -0
  30. {devservices-1.0.8 → devservices-1.0.9}/devservices/constants.py +0 -0
  31. {devservices-1.0.8 → devservices-1.0.9}/devservices/exceptions.py +0 -0
  32. {devservices-1.0.8 → devservices-1.0.9}/devservices/main.py +0 -0
  33. {devservices-1.0.8 → devservices-1.0.9}/devservices/utils/__init__.py +0 -0
  34. {devservices-1.0.8 → devservices-1.0.9}/devservices/utils/check_for_update.py +0 -0
  35. {devservices-1.0.8 → devservices-1.0.9}/devservices/utils/devenv.py +0 -0
  36. {devservices-1.0.8 → devservices-1.0.9}/devservices/utils/docker.py +0 -0
  37. {devservices-1.0.8 → devservices-1.0.9}/devservices/utils/file_lock.py +0 -0
  38. {devservices-1.0.8 → devservices-1.0.9}/devservices/utils/install_binary.py +0 -0
  39. {devservices-1.0.8 → devservices-1.0.9}/devservices/utils/state.py +0 -0
  40. {devservices-1.0.8 → devservices-1.0.9}/devservices.egg-info/SOURCES.txt +0 -0
  41. {devservices-1.0.8 → devservices-1.0.9}/devservices.egg-info/dependency_links.txt +0 -0
  42. {devservices-1.0.8 → devservices-1.0.9}/devservices.egg-info/entry_points.txt +0 -0
  43. {devservices-1.0.8 → devservices-1.0.9}/devservices.egg-info/requires.txt +0 -0
  44. {devservices-1.0.8 → devservices-1.0.9}/devservices.egg-info/top_level.txt +0 -0
  45. {devservices-1.0.8 → devservices-1.0.9}/setup.cfg +0 -0
  46. {devservices-1.0.8 → devservices-1.0.9}/testing/__init__.py +0 -0
  47. {devservices-1.0.8 → devservices-1.0.9}/testing/utils.py +0 -0
  48. {devservices-1.0.8 → devservices-1.0.9}/tests/__init__.py +0 -0
  49. {devservices-1.0.8 → devservices-1.0.9}/tests/commands/test_list_services.py +0 -0
  50. {devservices-1.0.8 → devservices-1.0.9}/tests/commands/test_purge.py +0 -0
  51. {devservices-1.0.8 → devservices-1.0.9}/tests/commands/test_update.py +0 -0
  52. {devservices-1.0.8 → devservices-1.0.9}/tests/conftest.py +0 -0
  53. {devservices-1.0.8 → devservices-1.0.9}/tests/utils/test_check_for_update.py +0 -0
  54. {devservices-1.0.8 → devservices-1.0.9}/tests/utils/test_dependencies.py +0 -0
  55. {devservices-1.0.8 → devservices-1.0.9}/tests/utils/test_docker.py +0 -0
  56. {devservices-1.0.8 → devservices-1.0.9}/tests/utils/test_install_binary.py +0 -0
  57. {devservices-1.0.8 → devservices-1.0.9}/tests/utils/test_state.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: devservices
3
- Version: 1.0.8
3
+ Version: 1.0.9
4
4
  Requires-Python: >=3.10
5
5
  License-File: LICENSE.md
6
6
  Requires-Dist: pyyaml
@@ -11,7 +11,7 @@ A standalone cli tool used to manage dependencies for services. It simplifies th
11
11
  The recommended way to install devservices is through a virtualenv in the requirements.txt.
12
12
 
13
13
  ```
14
- devservices==1.0.8
14
+ devservices==1.0.9
15
15
  ```
16
16
 
17
17
 
@@ -15,11 +15,13 @@ from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
15
15
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
16
16
  from devservices.constants import DEVSERVICES_DIR_NAME
17
17
  from devservices.exceptions import ConfigError
18
+ from devservices.exceptions import ConfigNotFoundError
18
19
  from devservices.exceptions import DependencyError
19
20
  from devservices.exceptions import DockerComposeError
20
21
  from devservices.exceptions import ServiceNotFoundError
21
22
  from devservices.utils.console import Console
22
23
  from devservices.utils.console import Status
24
+ from devservices.utils.dependencies import construct_dependency_graph
23
25
  from devservices.utils.dependencies import get_non_shared_remote_dependencies
24
26
  from devservices.utils.dependencies import install_and_verify_dependencies
25
27
  from devservices.utils.dependencies import InstalledRemoteDependency
@@ -56,6 +58,12 @@ def down(args: Namespace) -> None:
56
58
  service_name = args.service_name
57
59
  try:
58
60
  service = find_matching_service(service_name)
61
+ except ConfigNotFoundError as e:
62
+ capture_exception(e)
63
+ console.failure(
64
+ f"{str(e)}. Please specify a service (i.e. `devservices down sentry`) or run the command from a directory with a devservices configuration."
65
+ )
66
+ exit(1)
59
67
  except ConfigError as e:
60
68
  capture_exception(e)
61
69
  console.failure(str(e))
@@ -80,7 +88,6 @@ def down(args: Namespace) -> None:
80
88
 
81
89
  with Status(
82
90
  lambda: console.warning(f"Stopping {service.name}"),
83
- lambda: console.success(f"{service.name} stopped"),
84
91
  ) as status:
85
92
  try:
86
93
  remote_dependencies = install_and_verify_dependencies(
@@ -98,16 +105,42 @@ def down(args: Namespace) -> None:
98
105
  capture_exception(de)
99
106
  status.failure(str(de))
100
107
  exit(1)
101
- try:
102
- _down(service, remote_dependencies, list(mode_dependencies), status)
103
- except DockerComposeError as dce:
104
- capture_exception(dce)
105
- status.failure(f"Failed to stop {service.name}: {dce.stderr}")
106
- exit(1)
108
+
109
+ # Check if any service depends on the service we are trying to bring down
110
+ # TODO: We should also take into account the active modes of the other services (this is not trivial to do)
111
+ other_started_services = set(started_services).difference({service.name})
112
+ dependent_service_name = None
113
+ for other_started_service in other_started_services:
114
+ other_service = find_matching_service(other_started_service)
115
+ other_service_active_modes = state.get_active_modes_for_service(
116
+ other_service.name
117
+ )
118
+ dependency_graph = construct_dependency_graph(
119
+ other_service, other_service_active_modes
120
+ )
121
+ # If the service we are trying to bring down is in the dependency graph of another service, we should not bring it down
122
+ if service.name in dependency_graph.graph:
123
+ dependent_service_name = other_started_service
124
+ break
125
+
126
+ # If no other service depends on the service we are trying to bring down, we can bring it down
127
+ if dependent_service_name is None:
128
+ try:
129
+ _down(service, remote_dependencies, list(mode_dependencies), status)
130
+ except DockerComposeError as dce:
131
+ capture_exception(dce)
132
+ status.failure(f"Failed to stop {service.name}: {dce.stderr}")
133
+ exit(1)
134
+ else:
135
+ status.warning(
136
+ f"Leaving {service.name} running because it is being used by {dependent_service_name}"
137
+ )
107
138
 
108
139
  # TODO: We should factor in healthchecks here before marking service as not running
109
140
  state = State()
110
141
  state.remove_started_service(service.name)
142
+ if dependent_service_name is None:
143
+ console.success(f"{service.name} stopped")
111
144
 
112
145
 
113
146
  def _bring_down_dependency(
@@ -141,7 +174,7 @@ def _down(
141
174
  service=service,
142
175
  remote_dependencies=list(remote_dependencies),
143
176
  current_env=current_env,
144
- command="down",
177
+ command="stop",
145
178
  options=[],
146
179
  service_config_file_path=service_config_file_path,
147
180
  mode_dependencies=mode_dependencies,
@@ -7,6 +7,7 @@ from argparse import Namespace
7
7
  from sentry_sdk import capture_exception
8
8
 
9
9
  from devservices.exceptions import ConfigError
10
+ from devservices.exceptions import ConfigNotFoundError
10
11
  from devservices.exceptions import ServiceNotFoundError
11
12
  from devservices.utils.console import Console
12
13
  from devservices.utils.services import find_matching_service
@@ -32,6 +33,12 @@ def list_dependencies(args: Namespace) -> None:
32
33
 
33
34
  try:
34
35
  service = find_matching_service(service_name)
36
+ except ConfigNotFoundError as e:
37
+ capture_exception(e)
38
+ console.failure(
39
+ f"{str(e)}. Please specify a service (i.e. `devservices list-dependencies sentry`) or run the command from a directory with a devservices configuration."
40
+ )
41
+ exit(1)
35
42
  except ConfigError as e:
36
43
  capture_exception(e)
37
44
  console.failure(str(e))
@@ -16,6 +16,7 @@ from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
16
16
  from devservices.constants import DEVSERVICES_DIR_NAME
17
17
  from devservices.constants import MAX_LOG_LINES
18
18
  from devservices.exceptions import ConfigError
19
+ from devservices.exceptions import ConfigNotFoundError
19
20
  from devservices.exceptions import DependencyError
20
21
  from devservices.exceptions import DockerComposeError
21
22
  from devservices.exceptions import ServiceNotFoundError
@@ -46,6 +47,12 @@ def logs(args: Namespace) -> None:
46
47
  service_name = args.service_name
47
48
  try:
48
49
  service = find_matching_service(service_name)
50
+ except ConfigNotFoundError as e:
51
+ capture_exception(e)
52
+ console.failure(
53
+ f"{str(e)}. Please specify a service (i.e. `devservices logs sentry`) or run the command from a directory with a devservices configuration."
54
+ )
55
+ exit(1)
49
56
  except ConfigError as e:
50
57
  capture_exception(e)
51
58
  console.failure(str(e))
@@ -17,6 +17,7 @@ from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
17
17
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
18
18
  from devservices.constants import DEVSERVICES_DIR_NAME
19
19
  from devservices.exceptions import ConfigError
20
+ from devservices.exceptions import ConfigNotFoundError
20
21
  from devservices.exceptions import DependencyError
21
22
  from devservices.exceptions import DockerComposeError
22
23
  from devservices.exceptions import ServiceNotFoundError
@@ -86,6 +87,12 @@ def status(args: Namespace) -> None:
86
87
  service_name = args.service_name
87
88
  try:
88
89
  service = find_matching_service(service_name)
90
+ except ConfigNotFoundError as e:
91
+ capture_exception(e)
92
+ console.failure(
93
+ f"{str(e)}. Please specify a service (i.e. `devservices status sentry`) or run the command from a directory with a devservices configuration."
94
+ )
95
+ exit(1)
89
96
  except ConfigError as e:
90
97
  capture_exception(e)
91
98
  console.failure(str(e))
@@ -15,6 +15,7 @@ from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
15
15
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
16
16
  from devservices.constants import DEVSERVICES_DIR_NAME
17
17
  from devservices.exceptions import ConfigError
18
+ from devservices.exceptions import ConfigNotFoundError
18
19
  from devservices.exceptions import ContainerHealthcheckFailedError
19
20
  from devservices.exceptions import DependencyError
20
21
  from devservices.exceptions import DockerComposeError
@@ -60,6 +61,12 @@ def up(args: Namespace) -> None:
60
61
  service_name = args.service_name
61
62
  try:
62
63
  service = find_matching_service(service_name)
64
+ except ConfigNotFoundError as e:
65
+ capture_exception(e)
66
+ console.failure(
67
+ f"{str(e)}. Please specify a service (i.e. `devservices up sentry`) or run the command from a directory with a devservices configuration."
68
+ )
69
+ exit(1)
63
70
  except ConfigError as e:
64
71
  capture_exception(e)
65
72
  console.failure(str(e))
@@ -67,7 +67,9 @@ class ServiceConfig:
67
67
  def load_service_config_from_file(repo_path: str) -> ServiceConfig:
68
68
  config_path = os.path.join(repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME)
69
69
  if not os.path.exists(config_path):
70
- raise ConfigNotFoundError(f"Config file not found in directory: {config_path}")
70
+ raise ConfigNotFoundError(
71
+ f"No devservices configuration found in {config_path}"
72
+ )
71
73
  with open(config_path, "r", encoding="utf-8") as stream:
72
74
  try:
73
75
  config = yaml.safe_load(stream)
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import os
3
4
  import sys
4
5
  import threading
5
6
  import time
@@ -97,6 +98,8 @@ class Status:
97
98
  self.on_success()
98
99
 
99
100
  def _loading_animation(self) -> None:
101
+ if os.environ.get("CI", default="false") == "true":
102
+ return
100
103
  idx = 0
101
104
  while not self._stop_loading.is_set():
102
105
  sys.stdout.write("\r" + ANIMATION_FRAMES[idx % len(ANIMATION_FRAMES)] + " ")
@@ -5,6 +5,7 @@ import os
5
5
  import shutil
6
6
  import subprocess
7
7
  import tempfile
8
+ import time
8
9
  from collections import deque
9
10
  from concurrent.futures import as_completed
10
11
  from concurrent.futures import ThreadPoolExecutor
@@ -264,15 +265,28 @@ def get_non_shared_remote_dependencies(
264
265
  # We don't care about the remote dependencies of the service we are stopping
265
266
  started_services.remove(service_to_stop.name)
266
267
  other_running_remote_dependencies: set[InstalledRemoteDependency] = set()
267
- for service_name in started_services:
268
- service = find_matching_service(service_name)
268
+ base_running_service_names: set[str] = set()
269
+ for started_service_name in started_services:
270
+ started_service = find_matching_service(started_service_name)
271
+ for dependency_name in service_to_stop.config.dependencies.keys():
272
+ if dependency_name == started_service.config.service_name:
273
+ base_running_service_names.add(started_service_name)
274
+ installed_remote_dependencies = get_installed_remote_dependencies(
275
+ list(started_service.config.dependencies.values())
276
+ )
269
277
  # TODO: There is an edge case here where there is a shared remote dependency with different modes
270
278
  other_running_remote_dependencies = other_running_remote_dependencies.union(
271
- get_installed_remote_dependencies(
272
- list(service.config.dependencies.values())
273
- )
279
+ installed_remote_dependencies
274
280
  )
275
- return remote_dependencies.difference(other_running_remote_dependencies)
281
+ non_shared_remote_dependencies = remote_dependencies.difference(
282
+ other_running_remote_dependencies
283
+ )
284
+ non_shared_remote_dependencies = {
285
+ dependency
286
+ for dependency in non_shared_remote_dependencies
287
+ if dependency.service_name not in base_running_service_names
288
+ }
289
+ return non_shared_remote_dependencies
276
290
 
277
291
 
278
292
  def get_installed_remote_dependencies(
@@ -448,7 +462,7 @@ def _update_dependency(
448
462
  ) from e
449
463
 
450
464
  try:
451
- _run_command(
465
+ _run_command_with_retries(
452
466
  ["git", "fetch", "origin", dependency.branch, "--filter=blob:none"],
453
467
  cwd=dependency_repo_dir,
454
468
  )
@@ -574,6 +588,25 @@ def _run_command(
574
588
  subprocess.run(cmd, cwd=cwd, check=True, stdout=stdout, stderr=subprocess.DEVNULL)
575
589
 
576
590
 
591
+ def _run_command_with_retries(
592
+ cmd: list[str],
593
+ cwd: str,
594
+ stdout: int | TextIO | None = subprocess.DEVNULL,
595
+ retries: int = 3,
596
+ backoff: int = 2,
597
+ ) -> None:
598
+ for i in range(retries):
599
+ try:
600
+ _run_command(cmd, cwd=cwd, stdout=stdout)
601
+ break
602
+ except subprocess.CalledProcessError as e:
603
+ logger = logging.getLogger(LOGGER_NAME)
604
+ logger.exception("Attempt %s of %s failed: %s", i + 1, retries, e)
605
+ if i == retries - 1:
606
+ raise e
607
+ time.sleep(backoff**i)
608
+
609
+
577
610
  def _try_set_git_config_context(
578
611
  git_config_manager: GitConfigManager,
579
612
  ) -> None:
@@ -5,7 +5,6 @@ import os
5
5
  import platform
6
6
  import re
7
7
  import subprocess
8
- from collections.abc import Callable
9
8
  from typing import cast
10
9
  from typing import NamedTuple
11
10
 
@@ -170,7 +169,7 @@ def check_docker_compose_version() -> None:
170
169
 
171
170
 
172
171
  # TODO: Consider removing this in favor of in house logic for determining non-remote services
173
- def _get_non_remote_services(
172
+ def get_non_remote_services(
174
173
  service_config_path: str, current_env: dict[str, str]
175
174
  ) -> set[str]:
176
175
  config_command = [
@@ -195,19 +194,14 @@ def _get_non_remote_services(
195
194
  return set(config_services.splitlines())
196
195
 
197
196
 
198
- def get_docker_compose_commands_to_run(
199
- service: Service,
200
- remote_dependencies: list[InstalledRemoteDependency],
201
- current_env: dict[str, str],
197
+ def create_docker_compose_command(
198
+ name: str,
199
+ config_path: str,
200
+ services_to_use: set[str],
202
201
  command: str,
203
202
  options: list[str],
204
- service_config_file_path: str,
205
- mode_dependencies: list[str],
206
- ) -> list[DockerComposeCommand]:
207
- docker_compose_commands = []
208
- create_docker_compose_command: Callable[
209
- [str, str, set[str]], DockerComposeCommand
210
- ] = lambda name, config_path, services_to_use: DockerComposeCommand(
203
+ ) -> DockerComposeCommand:
204
+ return DockerComposeCommand(
211
205
  full_command=[
212
206
  "docker",
213
207
  "compose",
@@ -223,13 +217,25 @@ def get_docker_compose_commands_to_run(
223
217
  config_path=config_path,
224
218
  services=sorted(list(services_to_use)),
225
219
  )
220
+
221
+
222
+ def get_docker_compose_commands_to_run(
223
+ service: Service,
224
+ remote_dependencies: list[InstalledRemoteDependency],
225
+ current_env: dict[str, str],
226
+ command: str,
227
+ options: list[str],
228
+ service_config_file_path: str,
229
+ mode_dependencies: list[str],
230
+ ) -> list[DockerComposeCommand]:
231
+ docker_compose_commands = []
226
232
  for dependency in remote_dependencies:
227
233
  # TODO: Consider passing in service config in InstalledRemoteDependency instead of loading it here
228
234
  dependency_service_config = load_service_config_from_file(dependency.repo_path)
229
235
  dependency_config_path = os.path.join(
230
236
  dependency.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
231
237
  )
232
- non_remote_services = _get_non_remote_services(
238
+ non_remote_services = get_non_remote_services(
233
239
  dependency_config_path, current_env
234
240
  )
235
241
  services_to_use = non_remote_services.intersection(
@@ -240,18 +246,22 @@ def get_docker_compose_commands_to_run(
240
246
  dependency_service_config.service_name,
241
247
  dependency_config_path,
242
248
  services_to_use,
249
+ command,
250
+ options,
243
251
  )
244
252
  )
245
253
 
246
254
  # Add docker compose command for the top level service
247
- non_remote_services = _get_non_remote_services(
248
- service_config_file_path, current_env
249
- )
255
+ non_remote_services = get_non_remote_services(service_config_file_path, current_env)
250
256
  services_to_use = non_remote_services.intersection(set(mode_dependencies))
251
257
  if len(services_to_use) > 0:
252
258
  docker_compose_commands.append(
253
259
  create_docker_compose_command(
254
- service.name, service_config_file_path, services_to_use
260
+ service.name,
261
+ service_config_file_path,
262
+ services_to_use,
263
+ command,
264
+ options,
255
265
  )
256
266
  )
257
267
  return docker_compose_commands
@@ -65,4 +65,11 @@ def find_matching_service(service_name: str | None = None) -> Service:
65
65
  for service in services:
66
66
  if service.name.lower() == service_name.lower():
67
67
  return service
68
- raise ServiceNotFoundError(f'Service "{service_name}" not found')
68
+ unique_service_names = sorted(set(service.name for service in services))
69
+ error_message = f"Service '{service_name}' not found."
70
+ if len(unique_service_names) > 0:
71
+ service_bullet_points = "\n".join(
72
+ [f"- {service_name}" for service_name in unique_service_names]
73
+ )
74
+ error_message += "\nSupported services:\n" + service_bullet_points
75
+ raise ServiceNotFoundError(error_message)
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: devservices
3
- Version: 1.0.8
3
+ Version: 1.0.9
4
4
  Requires-Python: >=3.10
5
5
  License-File: LICENSE.md
6
6
  Requires-Dist: pyyaml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devservices"
7
- version = "1.0.8"
7
+ version = "1.0.9"
8
8
  # 3.10 is just for internal pypi compat
9
9
  requires-python = ">=3.10"
10
10
  dependencies = [