devservices 1.1.1__tar.gz → 1.1.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. {devservices-1.1.1 → devservices-1.1.3}/PKG-INFO +3 -2
  2. {devservices-1.1.1 → devservices-1.1.3}/README.md +1 -1
  3. devservices-1.1.3/devenv/sync.py +24 -0
  4. {devservices-1.1.1 → devservices-1.1.3}/devservices/commands/down.py +56 -16
  5. devservices-1.1.3/devservices/commands/serve.py +68 -0
  6. {devservices-1.1.1 → devservices-1.1.3}/devservices/commands/toggle.py +11 -6
  7. {devservices-1.1.1 → devservices-1.1.3}/devservices/commands/up.py +33 -9
  8. {devservices-1.1.1 → devservices-1.1.3}/devservices/constants.py +1 -0
  9. {devservices-1.1.1 → devservices-1.1.3}/devservices/main.py +2 -0
  10. {devservices-1.1.1 → devservices-1.1.3}/devservices/utils/dependencies.py +14 -4
  11. {devservices-1.1.1 → devservices-1.1.3}/devservices/utils/supervisor.py +20 -8
  12. {devservices-1.1.1 → devservices-1.1.3}/devservices.egg-info/PKG-INFO +3 -2
  13. {devservices-1.1.1 → devservices-1.1.3}/devservices.egg-info/SOURCES.txt +3 -0
  14. {devservices-1.1.1 → devservices-1.1.3}/devservices.egg-info/requires.txt +1 -0
  15. {devservices-1.1.1 → devservices-1.1.3}/devservices.egg-info/top_level.txt +1 -0
  16. {devservices-1.1.1 → devservices-1.1.3}/pyproject.toml +4 -3
  17. {devservices-1.1.1 → devservices-1.1.3}/testing/utils.py +9 -0
  18. {devservices-1.1.1 → devservices-1.1.3}/tests/commands/test_down.py +56 -40
  19. devservices-1.1.3/tests/commands/test_serve.py +163 -0
  20. {devservices-1.1.1 → devservices-1.1.3}/tests/commands/test_toggle.py +39 -16
  21. {devservices-1.1.1 → devservices-1.1.3}/tests/commands/test_up.py +445 -128
  22. {devservices-1.1.1 → devservices-1.1.3}/tests/utils/test_dependencies.py +177 -6
  23. {devservices-1.1.1 → devservices-1.1.3}/tests/utils/test_supervisor.py +30 -12
  24. {devservices-1.1.1 → devservices-1.1.3}/LICENSE.md +0 -0
  25. {devservices-1.1.1 → devservices-1.1.3}/devservices/__init__.py +0 -0
  26. {devservices-1.1.1 → devservices-1.1.3}/devservices/commands/__init__.py +0 -0
  27. {devservices-1.1.1 → devservices-1.1.3}/devservices/commands/list_dependencies.py +0 -0
  28. {devservices-1.1.1 → devservices-1.1.3}/devservices/commands/list_services.py +0 -0
  29. {devservices-1.1.1 → devservices-1.1.3}/devservices/commands/logs.py +0 -0
  30. {devservices-1.1.1 → devservices-1.1.3}/devservices/commands/purge.py +0 -0
  31. {devservices-1.1.1 → devservices-1.1.3}/devservices/commands/status.py +0 -0
  32. {devservices-1.1.1 → devservices-1.1.3}/devservices/commands/update.py +0 -0
  33. {devservices-1.1.1 → devservices-1.1.3}/devservices/configs/service_config.py +0 -0
  34. {devservices-1.1.1 → devservices-1.1.3}/devservices/exceptions.py +0 -0
  35. {devservices-1.1.1 → devservices-1.1.3}/devservices/utils/__init__.py +0 -0
  36. {devservices-1.1.1 → devservices-1.1.3}/devservices/utils/check_for_update.py +0 -0
  37. {devservices-1.1.1 → devservices-1.1.3}/devservices/utils/console.py +0 -0
  38. {devservices-1.1.1 → devservices-1.1.3}/devservices/utils/devenv.py +0 -0
  39. {devservices-1.1.1 → devservices-1.1.3}/devservices/utils/docker.py +0 -0
  40. {devservices-1.1.1 → devservices-1.1.3}/devservices/utils/docker_compose.py +0 -0
  41. {devservices-1.1.1 → devservices-1.1.3}/devservices/utils/file_lock.py +0 -0
  42. {devservices-1.1.1 → devservices-1.1.3}/devservices/utils/git.py +0 -0
  43. {devservices-1.1.1 → devservices-1.1.3}/devservices/utils/install_binary.py +0 -0
  44. {devservices-1.1.1 → devservices-1.1.3}/devservices/utils/services.py +0 -0
  45. {devservices-1.1.1 → devservices-1.1.3}/devservices/utils/state.py +0 -0
  46. {devservices-1.1.1 → devservices-1.1.3}/devservices.egg-info/dependency_links.txt +0 -0
  47. {devservices-1.1.1 → devservices-1.1.3}/devservices.egg-info/entry_points.txt +0 -0
  48. {devservices-1.1.1 → devservices-1.1.3}/setup.cfg +0 -0
  49. {devservices-1.1.1 → devservices-1.1.3}/testing/__init__.py +0 -0
  50. {devservices-1.1.1 → devservices-1.1.3}/tests/__init__.py +0 -0
  51. {devservices-1.1.1 → devservices-1.1.3}/tests/commands/test_list_dependencies.py +0 -0
  52. {devservices-1.1.1 → devservices-1.1.3}/tests/commands/test_list_services.py +0 -0
  53. {devservices-1.1.1 → devservices-1.1.3}/tests/commands/test_logs.py +0 -0
  54. {devservices-1.1.1 → devservices-1.1.3}/tests/commands/test_purge.py +0 -0
  55. {devservices-1.1.1 → devservices-1.1.3}/tests/commands/test_status.py +0 -0
  56. {devservices-1.1.1 → devservices-1.1.3}/tests/commands/test_update.py +0 -0
  57. {devservices-1.1.1 → devservices-1.1.3}/tests/configs/test_service_config.py +0 -0
  58. {devservices-1.1.1 → devservices-1.1.3}/tests/conftest.py +0 -0
  59. {devservices-1.1.1 → devservices-1.1.3}/tests/utils/test_check_for_update.py +0 -0
  60. {devservices-1.1.1 → devservices-1.1.3}/tests/utils/test_docker.py +0 -0
  61. {devservices-1.1.1 → devservices-1.1.3}/tests/utils/test_docker_compose.py +0 -0
  62. {devservices-1.1.1 → devservices-1.1.3}/tests/utils/test_git.py +0 -0
  63. {devservices-1.1.1 → devservices-1.1.3}/tests/utils/test_install_binary.py +0 -0
  64. {devservices-1.1.1 → devservices-1.1.3}/tests/utils/test_services.py +0 -0
  65. {devservices-1.1.1 → devservices-1.1.3}/tests/utils/test_state.py +0 -0
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devservices
3
- Version: 1.1.1
4
- Requires-Python: >=3.10
3
+ Version: 1.1.3
4
+ Requires-Python: >=3.11
5
5
  License-File: LICENSE.md
6
6
  Requires-Dist: pyyaml
7
7
  Requires-Dist: sentry-devenv
8
8
  Requires-Dist: sentry-sdk
9
9
  Requires-Dist: packaging
10
+ Requires-Dist: supervisor
10
11
  Provides-Extra: dev
11
12
  Requires-Dist: black; extra == "dev"
12
13
  Requires-Dist: mypy; extra == "dev"
@@ -31,7 +31,7 @@ NOTE: service-name is an optional parameter. If not provided, devservices will a
31
31
  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.
32
32
 
33
33
  ```
34
- devservices==1.1.1
34
+ devservices==1.1.3
35
35
  ```
36
36
 
37
37
  ### 2. Add devservices config files
@@ -0,0 +1,24 @@
1
+ from __future__ import annotations
2
+
3
+ from devenv.lib import config
4
+ from devenv.lib import proc
5
+ from devenv.lib import venv
6
+
7
+
8
+ def main(context: dict[str, str]) -> int:
9
+ reporoot = context["reporoot"]
10
+
11
+ venv_dir, python_version, requirements, editable_paths, bins = venv.get(
12
+ reporoot,
13
+ "venv",
14
+ )
15
+ url, sha256 = config.get_python(reporoot, python_version)
16
+ print(f"ensuring venv at {venv_dir}...")
17
+ venv.ensure(venv_dir, python_version, url, sha256)
18
+
19
+ print(f"syncing venv with {requirements}...")
20
+ venv.sync(reporoot, venv_dir, requirements, editable_paths, bins)
21
+
22
+ proc.run(("pre-commit", "install", "--install-hooks", "-f"))
23
+
24
+ return 0
@@ -53,6 +53,12 @@ def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
53
53
  action="store_true",
54
54
  default=False,
55
55
  )
56
+ parser.add_argument(
57
+ "--exclude-local",
58
+ help="Exclude dependencies with local runtime from being brought down",
59
+ action="store_true",
60
+ default=False,
61
+ )
56
62
  parser.set_defaults(func=down)
57
63
 
58
64
 
@@ -77,6 +83,7 @@ def down(args: Namespace) -> None:
77
83
  exit(1)
78
84
 
79
85
  modes = service.config.modes
86
+ exclude_local = args.exclude_local
80
87
 
81
88
  state = State()
82
89
  starting_services = set(state.get_service_entries(StateTables.STARTING_SERVICES))
@@ -113,7 +120,7 @@ def down(args: Namespace) -> None:
113
120
  exit(1)
114
121
  try:
115
122
  remote_dependencies = get_non_shared_remote_dependencies(
116
- service, remote_dependencies
123
+ service, remote_dependencies, exclude_local
117
124
  )
118
125
  except DependencyError as de:
119
126
  capture_exception(de)
@@ -125,13 +132,13 @@ def down(args: Namespace) -> None:
125
132
  # Check if any service depends on the service we are trying to bring down
126
133
  # TODO: We should also take into account the active modes of the other services (this is not trivial to do)
127
134
  other_started_services = active_services.difference({service.name})
128
- services_with_local_runtime = state.get_services_by_runtime(
135
+ services_with_local_runtimes = state.get_services_by_runtime(
129
136
  ServiceRuntime.LOCAL
130
137
  )
131
138
  dependent_service_name = None
132
139
  # We can ignore checking if anything relies on the service
133
140
  # if it is a locally running service
134
- if service.name not in services_with_local_runtime:
141
+ if service.name not in services_with_local_runtimes:
135
142
  dependent_service_name = _get_dependent_service(
136
143
  service, other_started_services, state
137
144
  )
@@ -140,7 +147,11 @@ def down(args: Namespace) -> None:
140
147
  if dependent_service_name is None:
141
148
  try:
142
149
  bring_down_service(
143
- service, remote_dependencies, list(mode_dependencies), status
150
+ service,
151
+ remote_dependencies,
152
+ list(mode_dependencies),
153
+ exclude_local,
154
+ status,
144
155
  )
145
156
  except DockerComposeError as dce:
146
157
  capture_exception(dce, level="info")
@@ -154,6 +165,24 @@ def down(args: Namespace) -> None:
154
165
  # TODO: We should factor in healthchecks here before marking service as not running
155
166
  state.remove_service_entry(service.name, StateTables.STARTING_SERVICES)
156
167
  state.remove_service_entry(service.name, StateTables.STARTED_SERVICES)
168
+
169
+ dependencies_with_local_runtimes = set()
170
+ for service_with_local_runtime in services_with_local_runtimes:
171
+ if service_with_local_runtime in {
172
+ dep.service_name for dep in remote_dependencies
173
+ }:
174
+ dependencies_with_local_runtimes.add(service_with_local_runtime)
175
+
176
+ active_dependencies_with_local_runtimes = set()
177
+ for dependency_with_local_runtime in dependencies_with_local_runtimes:
178
+ if dependency_with_local_runtime in active_services:
179
+ active_dependencies_with_local_runtimes.add(dependency_with_local_runtime)
180
+
181
+ if not exclude_local and len(active_dependencies_with_local_runtimes) > 0:
182
+ status.warning("Stopping dependencies with local runtimes...")
183
+ for local_dependency in active_dependencies_with_local_runtimes:
184
+ down(Namespace(service_name=local_dependency, exclude_local=exclude_local))
185
+
157
186
  if dependent_service_name is None:
158
187
  console.success(f"{service.name} stopped")
159
188
 
@@ -162,6 +191,7 @@ def bring_down_service(
162
191
  service: Service,
163
192
  remote_dependencies: set[InstalledRemoteDependency],
164
193
  mode_dependencies: list[str],
194
+ exclude_local: bool,
165
195
  status: Status,
166
196
  ) -> None:
167
197
  relative_local_dependency_directory = os.path.relpath(
@@ -171,30 +201,40 @@ def bring_down_service(
171
201
  service_config_file_path = os.path.join(
172
202
  service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
173
203
  )
204
+
174
205
  # Set the environment variable for the local dependencies directory to be used by docker compose
175
206
  current_env = os.environ.copy()
176
207
  current_env[
177
208
  DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
178
209
  ] = relative_local_dependency_directory
179
210
  state = State()
180
- # We want to ignore any dependencies that are set to run locally
181
- locally_running_services = state.get_services_by_runtime(ServiceRuntime.LOCAL)
182
- mode_dependencies = [
183
- dep for dep in mode_dependencies if dep not in locally_running_services
184
- ]
185
- remote_dependencies = {
186
- dep
187
- for dep in remote_dependencies
188
- if dep.service_name not in locally_running_services
189
- }
211
+
212
+ # We want to ignore any dependencies that are set to run locally if we are excluding local dependencies
213
+ services_with_local_runtimes = state.get_services_by_runtime(ServiceRuntime.LOCAL)
214
+
215
+ dependencies_with_local_runtimes = set()
216
+ for service_with_local_runtime in services_with_local_runtimes:
217
+ if service_with_local_runtime in {
218
+ dep.service_name for dep in remote_dependencies
219
+ }:
220
+ dependencies_with_local_runtimes.add(service_with_local_runtime)
221
+
190
222
  docker_compose_commands = get_docker_compose_commands_to_run(
191
223
  service=service,
192
- remote_dependencies=list(remote_dependencies),
224
+ remote_dependencies=[
225
+ dep
226
+ for dep in remote_dependencies
227
+ if dep.service_name not in dependencies_with_local_runtimes
228
+ ],
193
229
  current_env=current_env,
194
230
  command="stop",
195
231
  options=[],
196
232
  service_config_file_path=service_config_file_path,
197
- mode_dependencies=mode_dependencies,
233
+ mode_dependencies=[
234
+ dep
235
+ for dep in mode_dependencies
236
+ if dep not in dependencies_with_local_runtimes
237
+ ],
198
238
  )
199
239
 
200
240
  cmd_outputs = []
@@ -0,0 +1,68 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import pty
5
+ import shlex
6
+ from argparse import _SubParsersAction
7
+ from argparse import ArgumentParser
8
+ from argparse import Namespace
9
+
10
+ from sentry_sdk import capture_exception
11
+
12
+ from devservices.constants import DEVSERVICES_DIR_NAME
13
+ from devservices.constants import PROGRAMS_CONF_FILE_NAME
14
+ from devservices.exceptions import ConfigError
15
+ from devservices.exceptions import ConfigNotFoundError
16
+ from devservices.exceptions import SupervisorConfigError
17
+ from devservices.utils.console import Console
18
+ from devservices.utils.services import find_matching_service
19
+ from devservices.utils.supervisor import SupervisorManager
20
+
21
+
22
+ def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
23
+ # prefix_chars is a hack to allow all options to be passed through to the devserver without argparse complaining
24
+ parser = subparsers.add_parser(
25
+ "serve", help="Serve the devserver", prefix_chars="+"
26
+ )
27
+ parser.add_argument(
28
+ "extra", nargs="*", help="Flags to pass through to the devserver"
29
+ )
30
+ parser.set_defaults(func=serve)
31
+
32
+
33
+ def serve(args: Namespace) -> None:
34
+ """Serve the devserver."""
35
+ console = Console()
36
+
37
+ try:
38
+ service = find_matching_service()
39
+ except ConfigNotFoundError as e:
40
+ console.failure(
41
+ f"{str(e)}. Please run the command from a directory with a valid devservices configuration."
42
+ )
43
+ return
44
+ except ConfigError as e:
45
+ capture_exception(e)
46
+ console.failure(str(e))
47
+ exit(1)
48
+
49
+ programs_config_path = os.path.join(
50
+ service.repo_path, f"{DEVSERVICES_DIR_NAME}/{PROGRAMS_CONF_FILE_NAME}"
51
+ )
52
+ if not os.path.exists(programs_config_path):
53
+ console.failure(f"No programs.conf file found in {programs_config_path}.")
54
+ return
55
+ manager = SupervisorManager(
56
+ programs_config_path,
57
+ service_name=service.name,
58
+ )
59
+
60
+ try:
61
+ devserver_command = manager.get_program_command("devserver")
62
+ except SupervisorConfigError as e:
63
+ capture_exception(e, level="info")
64
+ console.failure(f"Error when getting devserver command: {str(e)}")
65
+ return
66
+
67
+ argv = shlex.split(devserver_command) + args.extra
68
+ pty.spawn(argv)
@@ -40,7 +40,7 @@ def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
40
40
  parser.add_argument(
41
41
  "runtime",
42
42
  help="Runtime to use for the service",
43
- choices=[ServiceRuntime.CONTAINERIZED, ServiceRuntime.LOCAL],
43
+ choices=[runtime.value for runtime in ServiceRuntime],
44
44
  nargs="?",
45
45
  default=None,
46
46
  )
@@ -71,7 +71,7 @@ def toggle(args: Namespace) -> None:
71
71
  state = State()
72
72
  current_runtime = state.get_service_runtime(service.name)
73
73
  if desired_runtime is None:
74
- desired_runtime = get_opposite_runtime(current_runtime)
74
+ desired_runtime = get_opposite_runtime(current_runtime).value
75
75
  if current_runtime == desired_runtime:
76
76
  console.warning(
77
77
  f"{service.name} is already running in {desired_runtime} runtime"
@@ -105,7 +105,7 @@ def handle_transition_to_local_runtime(service_to_transition: Service) -> None:
105
105
  if service_to_transition.name in active_services:
106
106
  state.update_service_runtime(service_to_transition.name, ServiceRuntime.LOCAL)
107
107
  console.success(
108
- f"{service_to_transition.name} is now running in {ServiceRuntime.LOCAL} runtime"
108
+ f"{service_to_transition.name} is now running in {ServiceRuntime.LOCAL.value} runtime"
109
109
  )
110
110
  return
111
111
 
@@ -196,7 +196,7 @@ def restart_dependent_services(
196
196
  console = Console()
197
197
  with Status(
198
198
  on_start=lambda: console.warning(
199
- f"Restarting dependent services to ensure {service_name} is running in a {ServiceRuntime.CONTAINERIZED} runtime"
199
+ f"Restarting dependent services to ensure {service_name} is running in a {ServiceRuntime.CONTAINERIZED.value} runtime"
200
200
  ),
201
201
  ) as status:
202
202
  for dependent_service in dependent_services:
@@ -223,6 +223,7 @@ def bring_down_containerized_service(
223
223
  ) -> None:
224
224
  """Bring down a containerized service running within another service."""
225
225
  console = Console()
226
+ exclude_local = True
226
227
  with Status(
227
228
  lambda: console.warning(f"Stopping {service.name}"),
228
229
  ) as status:
@@ -242,7 +243,7 @@ def bring_down_containerized_service(
242
243
  exit(1)
243
244
  try:
244
245
  remote_dependencies = get_non_shared_remote_dependencies(
245
- service, remote_dependencies
246
+ service, remote_dependencies, exclude_local
246
247
  )
247
248
  except DependencyError as de:
248
249
  capture_exception(de)
@@ -252,7 +253,11 @@ def bring_down_containerized_service(
252
253
  exit(1)
253
254
  try:
254
255
  bring_down_service(
255
- service, remote_dependencies, sorted(list(mode_dependencies)), status
256
+ service,
257
+ remote_dependencies,
258
+ sorted(list(mode_dependencies)),
259
+ exclude_local,
260
+ status,
256
261
  )
257
262
  except DockerComposeError as dce:
258
263
  capture_exception(dce, level="info")
@@ -56,10 +56,16 @@ def add_parser(subparsers: _SubParsersAction[ArgumentParser]) -> None:
56
56
  help="Mode to use for the service",
57
57
  default="default",
58
58
  )
59
+ parser.add_argument(
60
+ "--exclude-local",
61
+ help="Exclude dependencies with local runtime from being started",
62
+ action="store_true",
63
+ default=False,
64
+ )
59
65
  parser.set_defaults(func=up)
60
66
 
61
67
 
62
- def up(args: Namespace) -> None:
68
+ def up(args: Namespace, existing_status: Status | None = None) -> None:
63
69
  """Bring up a service and its dependencies."""
64
70
  console = Console()
65
71
  service_name = args.service_name
@@ -85,8 +91,12 @@ def up(args: Namespace) -> None:
85
91
  state = State()
86
92
 
87
93
  with Status(
88
- lambda: console.warning(f"Starting '{service.name}' in mode: '{mode}'"),
89
- lambda: console.success(f"{service.name} started"),
94
+ lambda: console.warning(f"Starting '{service.name}' in mode: '{mode}'")
95
+ if existing_status is None
96
+ else existing_status.warning(f"Starting '{service.name}' in mode: '{mode}'"),
97
+ lambda: console.success(f"{service.name} started")
98
+ if existing_status is None
99
+ else existing_status.success(f"{service.name} started"),
90
100
  ) as status:
91
101
  services_with_local_runtime = state.get_services_by_runtime(
92
102
  ServiceRuntime.LOCAL
@@ -99,9 +109,10 @@ def up(args: Namespace) -> None:
99
109
  and service_with_local_runtime in modes[mode]
100
110
  ):
101
111
  skipped_services.add(service_with_local_runtime)
102
- status.warning(
103
- f"Skipping '{service_with_local_runtime}' as it is set to run locally"
104
- )
112
+ if args.exclude_local:
113
+ status.warning(
114
+ f"Skipping '{service_with_local_runtime}' as it is set to run locally"
115
+ )
105
116
  try:
106
117
  status.info("Retrieving dependencies")
107
118
  remote_dependencies = install_and_verify_dependencies(
@@ -131,9 +142,10 @@ def up(args: Namespace) -> None:
131
142
  and service_with_local_runtime not in skipped_services
132
143
  ):
133
144
  skipped_services.add(service_with_local_runtime)
134
- status.warning(
135
- f"Skipping '{service_with_local_runtime}' as it is set to run locally"
136
- )
145
+ if args.exclude_local:
146
+ status.warning(
147
+ f"Skipping '{service_with_local_runtime}' as it is set to run locally"
148
+ )
137
149
  # We want to ignore any dependencies that are set to run locally
138
150
  mode_dependencies = [
139
151
  dep for dep in mode_dependencies if dep not in services_with_local_runtime
@@ -144,6 +156,18 @@ def up(args: Namespace) -> None:
144
156
  if dep.service_name not in services_with_local_runtime
145
157
  }
146
158
  try:
159
+ if not args.exclude_local:
160
+ status.warning("Starting dependencies with local runtimes...")
161
+ for skipped_service in skipped_services:
162
+ up(
163
+ Namespace(
164
+ service_name=skipped_service,
165
+ mode=mode,
166
+ exclude_local=True,
167
+ ),
168
+ status,
169
+ )
170
+ status.warning(f"Continuing with service '{service.name}'")
147
171
  _up(service, [mode], remote_dependencies, mode_dependencies, status)
148
172
  except DockerComposeError as dce:
149
173
  capture_exception(dce, level="info")
@@ -18,6 +18,7 @@ class Color:
18
18
  MINIMUM_DOCKER_COMPOSE_VERSION = "2.29.7"
19
19
  DEVSERVICES_DIR_NAME = "devservices"
20
20
  CONFIG_FILE_NAME = "config.yml"
21
+ PROGRAMS_CONF_FILE_NAME = "programs.conf"
21
22
  DOCKER_CONFIG_DIR = os.environ.get("DOCKER_CONFIG", os.path.expanduser("~/.docker"))
22
23
  DOCKER_USER_PLUGIN_DIR = os.path.join(DOCKER_CONFIG_DIR, "cli-plugins/")
23
24
 
@@ -24,6 +24,7 @@ from devservices.commands import list_dependencies
24
24
  from devservices.commands import list_services
25
25
  from devservices.commands import logs
26
26
  from devservices.commands import purge
27
+ from devservices.commands import serve
27
28
  from devservices.commands import status
28
29
  from devservices.commands import toggle
29
30
  from devservices.commands import up
@@ -147,6 +148,7 @@ def main() -> None:
147
148
  logs.add_parser(subparsers)
148
149
  update.add_parser(subparsers)
149
150
  purge.add_parser(subparsers)
151
+ serve.add_parser(subparsers)
150
152
  toggle.add_parser(subparsers)
151
153
 
152
154
  args = parser.parse_args()
@@ -39,6 +39,7 @@ from devservices.exceptions import UnableToCloneDependencyError
39
39
  from devservices.utils.file_lock import lock
40
40
  from devservices.utils.services import find_matching_service
41
41
  from devservices.utils.services import Service
42
+ from devservices.utils.state import ServiceRuntime
42
43
  from devservices.utils.state import State
43
44
  from devservices.utils.state import StateTables
44
45
 
@@ -279,7 +280,9 @@ def verify_local_dependencies(dependencies: list[Dependency]) -> bool:
279
280
 
280
281
 
281
282
  def get_non_shared_remote_dependencies(
282
- service_to_stop: Service, remote_dependencies: set[InstalledRemoteDependency]
283
+ service_to_stop: Service,
284
+ remote_dependencies: set[InstalledRemoteDependency],
285
+ exclude_local: bool,
283
286
  ) -> set[InstalledRemoteDependency]:
284
287
  state = State()
285
288
  starting_services = set(state.get_service_entries(StateTables.STARTING_SERVICES))
@@ -291,6 +294,7 @@ def get_non_shared_remote_dependencies(
291
294
 
292
295
  active_modes: dict[str, list[str]] = dict()
293
296
  for active_service in active_services:
297
+ # TODO: We probably shouldn't use an OR here, but an AND
294
298
  starting_modes = state.get_active_modes_for_service(
295
299
  active_service, StateTables.STARTING_SERVICES
296
300
  )
@@ -303,9 +307,15 @@ def get_non_shared_remote_dependencies(
303
307
  base_running_service_names: set[str] = set()
304
308
  for started_service_name in active_services:
305
309
  started_service = find_matching_service(started_service_name)
306
- for dependency_name in service_to_stop.config.dependencies.keys():
307
- if dependency_name == started_service.config.service_name:
308
- base_running_service_names.add(started_service_name)
310
+ started_service_runtime = state.get_service_runtime(started_service_name)
311
+ if exclude_local or started_service_runtime != ServiceRuntime.LOCAL:
312
+ # TODO: In theory, we should only be able to run the base-service when a dependent service is running if
313
+ # 1. the dependent service is using a mode that doesn't include the base-service
314
+ # 2. the base-service is using a local runtime
315
+ # But we don't restrict the other cases currently
316
+ for dependency_name in service_to_stop.config.dependencies.keys():
317
+ if dependency_name == started_service.config.service_name:
318
+ base_running_service_names.add(started_service_name)
309
319
 
310
320
  started_service_modes = active_modes[started_service_name]
311
321
  # Only consider the dependencies of the modes that are running
@@ -8,6 +8,8 @@ import subprocess
8
8
  import xmlrpc.client
9
9
  from enum import IntEnum
10
10
 
11
+ from supervisor.options import ServerOptions
12
+
11
13
  from devservices.constants import DEVSERVICES_SUPERVISOR_CONFIG_DIR
12
14
  from devservices.exceptions import SupervisorConfigError
13
15
  from devservices.exceptions import SupervisorConnectionError
@@ -151,26 +153,36 @@ class SupervisorManager:
151
153
  except xmlrpc.client.Fault as e:
152
154
  raise SupervisorError(f"Failed to stop supervisor: {e.faultString}")
153
155
 
154
- def start_program(self, program_name: str) -> None:
155
- if self._is_program_running(program_name):
156
+ def start_process(self, name: str) -> None:
157
+ if self._is_program_running(name):
156
158
  return
157
159
  try:
158
- self._get_rpc_client().supervisor.startProcess(program_name)
160
+ self._get_rpc_client().supervisor.startProcess(name)
159
161
  except xmlrpc.client.Fault as e:
160
162
  raise SupervisorProcessError(
161
- f"Failed to start program {program_name}: {e.faultString}"
163
+ f"Failed to start process {name}: {e.faultString}"
162
164
  )
163
165
 
164
- def stop_program(self, program_name: str) -> None:
165
- if not self._is_program_running(program_name):
166
+ def stop_process(self, name: str) -> None:
167
+ if not self._is_program_running(name):
166
168
  return
167
169
  try:
168
- self._get_rpc_client().supervisor.stopProcess(program_name)
170
+ self._get_rpc_client().supervisor.stopProcess(name)
169
171
  except xmlrpc.client.Fault as e:
170
172
  raise SupervisorProcessError(
171
- f"Failed to stop program {program_name}: {e.faultString}"
173
+ f"Failed to stop process {name}: {e.faultString}"
172
174
  )
173
175
 
176
+ def get_program_command(self, program_name: str) -> str:
177
+ opts = ServerOptions()
178
+ opts.configfile = self.config_file_path
179
+ opts.process_config()
180
+ for group in opts.process_group_configs:
181
+ for proc in group.process_configs:
182
+ if proc.name == program_name and isinstance(proc.command, str):
183
+ return proc.command
184
+ raise SupervisorConfigError(f"Program {program_name} not found in config")
185
+
174
186
  def tail_program_logs(self, program_name: str) -> None:
175
187
  if not self._is_program_running(program_name):
176
188
  console = Console()
@@ -1,12 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devservices
3
- Version: 1.1.1
4
- Requires-Python: >=3.10
3
+ Version: 1.1.3
4
+ Requires-Python: >=3.11
5
5
  License-File: LICENSE.md
6
6
  Requires-Dist: pyyaml
7
7
  Requires-Dist: sentry-devenv
8
8
  Requires-Dist: sentry-sdk
9
9
  Requires-Dist: packaging
10
+ Requires-Dist: supervisor
10
11
  Provides-Extra: dev
11
12
  Requires-Dist: black; extra == "dev"
12
13
  Requires-Dist: mypy; extra == "dev"
@@ -2,6 +2,7 @@ LICENSE.md
2
2
  README.md
3
3
  pyproject.toml
4
4
  setup.cfg
5
+ devenv/sync.py
5
6
  devservices/__init__.py
6
7
  devservices/constants.py
7
8
  devservices/exceptions.py
@@ -18,6 +19,7 @@ devservices/commands/list_dependencies.py
18
19
  devservices/commands/list_services.py
19
20
  devservices/commands/logs.py
20
21
  devservices/commands/purge.py
22
+ devservices/commands/serve.py
21
23
  devservices/commands/status.py
22
24
  devservices/commands/toggle.py
23
25
  devservices/commands/up.py
@@ -45,6 +47,7 @@ tests/commands/test_list_dependencies.py
45
47
  tests/commands/test_list_services.py
46
48
  tests/commands/test_logs.py
47
49
  tests/commands/test_purge.py
50
+ tests/commands/test_serve.py
48
51
  tests/commands/test_status.py
49
52
  tests/commands/test_toggle.py
50
53
  tests/commands/test_up.py
@@ -2,6 +2,7 @@ pyyaml
2
2
  sentry-devenv
3
3
  sentry-sdk
4
4
  packaging
5
+ supervisor
5
6
 
6
7
  [dev]
7
8
  black
@@ -1,3 +1,4 @@
1
+ devenv
1
2
  devservices
2
3
  dist
3
4
  scripts
@@ -4,14 +4,15 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devservices"
7
- version = "1.1.1"
8
- # 3.10 is just for internal pypi compat
9
- requires-python = ">=3.10"
7
+ version = "1.1.3"
8
+ # 3.11 is just for internal pypi compat
9
+ requires-python = ">=3.11"
10
10
  dependencies = [
11
11
  "pyyaml",
12
12
  "sentry-devenv",
13
13
  "sentry-sdk",
14
14
  "packaging",
15
+ "supervisor",
15
16
  ]
16
17
 
17
18
  [project.optional-dependencies]
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
  import yaml
9
9
 
10
10
  from devservices.constants import DEVSERVICES_DIR_NAME
11
+ from devservices.constants import PROGRAMS_CONF_FILE_NAME
11
12
 
12
13
  TESTING_DIR = os.path.abspath(os.path.dirname(__file__))
13
14
 
@@ -26,6 +27,14 @@ def create_config_file(
26
27
  yaml.dump(config, f, sort_keys=False, default_flow_style=False)
27
28
 
28
29
 
30
+ def create_programs_conf_file(tmp_path: Path, config: str) -> None:
31
+ devservices_dir = Path(tmp_path, DEVSERVICES_DIR_NAME)
32
+ devservices_dir.mkdir(parents=True, exist_ok=True)
33
+ tmp_file = Path(devservices_dir, PROGRAMS_CONF_FILE_NAME)
34
+ with tmp_file.open("w") as f:
35
+ f.write(config)
36
+
37
+
29
38
  def run_git_command(command: list[str], cwd: Path) -> None:
30
39
  subprocess.run(
31
40
  ["git", *command], cwd=cwd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL