devservices 1.2.0__tar.gz → 1.2.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.2.0 → devservices-1.2.1}/PKG-INFO +1 -1
  2. {devservices-1.2.0 → devservices-1.2.1}/README.md +3 -1
  3. {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/down.py +24 -10
  4. {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/foreground.py +8 -7
  5. {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/logs.py +3 -5
  6. {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/serve.py +17 -9
  7. {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/status.py +10 -8
  8. {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/up.py +23 -10
  9. {devservices-1.2.0 → devservices-1.2.1}/devservices/configs/service_config.py +14 -10
  10. {devservices-1.2.0 → devservices-1.2.1}/devservices/constants.py +0 -1
  11. {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/supervisor.py +73 -12
  12. {devservices-1.2.0 → devservices-1.2.1}/devservices.egg-info/PKG-INFO +1 -1
  13. {devservices-1.2.0 → devservices-1.2.1}/pyproject.toml +1 -1
  14. {devservices-1.2.0 → devservices-1.2.1}/testing/utils.py +0 -9
  15. {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_down.py +24 -59
  16. {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_foreground.py +59 -95
  17. {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_list_services.py +1 -1
  18. {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_logs.py +29 -45
  19. {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_serve.py +14 -19
  20. {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_status.py +39 -38
  21. {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_up.py +48 -35
  22. {devservices-1.2.0 → devservices-1.2.1}/tests/configs/test_service_config.py +54 -19
  23. {devservices-1.2.0 → devservices-1.2.1}/tests/utils/test_supervisor.py +103 -10
  24. {devservices-1.2.0 → devservices-1.2.1}/LICENSE.md +0 -0
  25. {devservices-1.2.0 → devservices-1.2.1}/devservices/__init__.py +0 -0
  26. {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/__init__.py +0 -0
  27. {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/list_dependencies.py +0 -0
  28. {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/list_services.py +0 -0
  29. {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/purge.py +0 -0
  30. {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/reset.py +0 -0
  31. {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/toggle.py +0 -0
  32. {devservices-1.2.0 → devservices-1.2.1}/devservices/commands/update.py +0 -0
  33. {devservices-1.2.0 → devservices-1.2.1}/devservices/exceptions.py +0 -0
  34. {devservices-1.2.0 → devservices-1.2.1}/devservices/main.py +0 -0
  35. {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/__init__.py +0 -0
  36. {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/check_for_update.py +0 -0
  37. {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/console.py +0 -0
  38. {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/dependencies.py +0 -0
  39. {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/devenv.py +0 -0
  40. {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/docker.py +0 -0
  41. {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/docker_compose.py +0 -0
  42. {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/file_lock.py +0 -0
  43. {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/git.py +0 -0
  44. {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/install_binary.py +0 -0
  45. {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/services.py +0 -0
  46. {devservices-1.2.0 → devservices-1.2.1}/devservices/utils/state.py +0 -0
  47. {devservices-1.2.0 → devservices-1.2.1}/devservices.egg-info/SOURCES.txt +0 -0
  48. {devservices-1.2.0 → devservices-1.2.1}/devservices.egg-info/dependency_links.txt +0 -0
  49. {devservices-1.2.0 → devservices-1.2.1}/devservices.egg-info/entry_points.txt +0 -0
  50. {devservices-1.2.0 → devservices-1.2.1}/devservices.egg-info/requires.txt +0 -0
  51. {devservices-1.2.0 → devservices-1.2.1}/devservices.egg-info/top_level.txt +0 -0
  52. {devservices-1.2.0 → devservices-1.2.1}/setup.cfg +0 -0
  53. {devservices-1.2.0 → devservices-1.2.1}/testing/__init__.py +0 -0
  54. {devservices-1.2.0 → devservices-1.2.1}/tests/__init__.py +0 -0
  55. {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_list_dependencies.py +0 -0
  56. {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_purge.py +0 -0
  57. {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_reset.py +0 -0
  58. {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_toggle.py +0 -0
  59. {devservices-1.2.0 → devservices-1.2.1}/tests/commands/test_update.py +0 -0
  60. {devservices-1.2.0 → devservices-1.2.1}/tests/conftest.py +0 -0
  61. {devservices-1.2.0 → devservices-1.2.1}/tests/utils/test_check_for_update.py +0 -0
  62. {devservices-1.2.0 → devservices-1.2.1}/tests/utils/test_dependencies.py +0 -0
  63. {devservices-1.2.0 → devservices-1.2.1}/tests/utils/test_docker.py +0 -0
  64. {devservices-1.2.0 → devservices-1.2.1}/tests/utils/test_docker_compose.py +0 -0
  65. {devservices-1.2.0 → devservices-1.2.1}/tests/utils/test_git.py +0 -0
  66. {devservices-1.2.0 → devservices-1.2.1}/tests/utils/test_install_binary.py +0 -0
  67. {devservices-1.2.0 → devservices-1.2.1}/tests/utils/test_services.py +0 -0
  68. {devservices-1.2.0 → devservices-1.2.1}/tests/utils/test_state.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devservices
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Requires-Python: >=3.11
5
5
  License-File: LICENSE.md
6
6
  Requires-Dist: pyyaml
@@ -23,6 +23,8 @@ NOTE: service-name is an optional parameter. If not provided, devservices will a
23
23
  - `devservices update` Update devservices to the latest version.
24
24
  - `devservices purge`: Purge the local devservices cache.
25
25
  - `devservices toggle <service-name>`: Toggle the runtime for a service between containerized and local.
26
+ - `devservices serve`: Run a service's devserver.
27
+ - `devservices foreground <process-name>`: Foreground a process that is currently running in the background. This enables interactive debugging.
26
28
 
27
29
  ## Installation
28
30
 
@@ -31,7 +33,7 @@ NOTE: service-name is an optional parameter. If not provided, devservices will a
31
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.
32
34
 
33
35
  ```
34
- devservices==1.2.0
36
+ devservices==1.2.1
35
37
  ```
36
38
 
37
39
  ### 2. Add devservices config files
@@ -15,12 +15,12 @@ from devservices.constants import DependencyType
15
15
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
16
16
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
17
17
  from devservices.constants import DEVSERVICES_DIR_NAME
18
- from devservices.constants import PROGRAMS_CONF_FILE_NAME
19
18
  from devservices.exceptions import ConfigError
20
19
  from devservices.exceptions import ConfigNotFoundError
21
20
  from devservices.exceptions import DependencyError
22
21
  from devservices.exceptions import DockerComposeError
23
22
  from devservices.exceptions import ServiceNotFoundError
23
+ from devservices.exceptions import SupervisorConfigError
24
24
  from devservices.exceptions import SupervisorError
25
25
  from devservices.utils.console import Console
26
26
  from devservices.utils.console import Status
@@ -305,22 +305,36 @@ def _bring_down_dependency(
305
305
  return run_cmd(cmd.full_command, current_env)
306
306
 
307
307
 
308
+ def _stop_supervisor_program(
309
+ manager: SupervisorManager, program: str, status: Status
310
+ ) -> None:
311
+ """Stop a single supervisor program."""
312
+ status.info(f"Stopping {program}")
313
+ manager.stop_process(program)
314
+
315
+
308
316
  def bring_down_supervisor_programs(
309
317
  supervisor_programs: list[str], service: Service, status: Status
310
318
  ) -> None:
311
319
  if len(supervisor_programs) == 0:
312
320
  return
313
- programs_config_path = os.path.join(
314
- service.repo_path, f"{DEVSERVICES_DIR_NAME}/{PROGRAMS_CONF_FILE_NAME}"
315
- )
316
- manager = SupervisorManager(
317
- programs_config_path,
318
- service_name=service.name,
321
+
322
+ config_file_path = os.path.join(
323
+ service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
319
324
  )
320
325
 
321
- for program in supervisor_programs:
322
- status.info(f"Stopping {program}")
323
- manager.stop_process(program)
326
+ try:
327
+ manager = SupervisorManager(service.name, config_file_path)
328
+ except SupervisorConfigError:
329
+ raise
330
+
331
+ with concurrent.futures.ThreadPoolExecutor() as executor:
332
+ futures = [
333
+ executor.submit(_stop_supervisor_program, manager, program, status)
334
+ for program in supervisor_programs
335
+ ]
336
+ for future in concurrent.futures.as_completed(futures):
337
+ _ = future.result()
324
338
 
325
339
  status.info("Stopping supervisor daemon")
326
340
  manager.stop_supervisor_daemon()
@@ -9,9 +9,9 @@ from argparse import Namespace
9
9
 
10
10
  from sentry_sdk import capture_exception
11
11
 
12
+ from devservices.constants import CONFIG_FILE_NAME
12
13
  from devservices.constants import DependencyType
13
14
  from devservices.constants import DEVSERVICES_DIR_NAME
14
- from devservices.constants import PROGRAMS_CONF_FILE_NAME
15
15
  from devservices.exceptions import ConfigError
16
16
  from devservices.exceptions import ConfigNotFoundError
17
17
  from devservices.exceptions import ServiceNotFoundError
@@ -92,14 +92,15 @@ def foreground(args: Namespace) -> None:
92
92
  )
93
93
  return
94
94
 
95
- programs_config_path = os.path.join(
96
- service.repo_path, f"{DEVSERVICES_DIR_NAME}/{PROGRAMS_CONF_FILE_NAME}"
95
+ config_file_path = os.path.join(
96
+ service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
97
97
  )
98
98
 
99
- manager = SupervisorManager(
100
- programs_config_path,
101
- service_name=service.name,
102
- )
99
+ try:
100
+ manager = SupervisorManager(service.name, config_file_path)
101
+ except SupervisorConfigError as e:
102
+ capture_exception(e, level="info")
103
+ return
103
104
 
104
105
  try:
105
106
  program_command = manager.get_program_command(program_name)
@@ -16,7 +16,6 @@ from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
16
16
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
17
17
  from devservices.constants import DEVSERVICES_DIR_NAME
18
18
  from devservices.constants import MAX_LOG_LINES
19
- from devservices.constants import PROGRAMS_CONF_FILE_NAME
20
19
  from devservices.exceptions import ConfigError
21
20
  from devservices.exceptions import ConfigNotFoundError
22
21
  from devservices.exceptions import DependencyError
@@ -178,12 +177,11 @@ def _supervisor_logs(
178
177
 
179
178
  supervisor_logs: dict[str, str] = {}
180
179
 
181
- programs_config_path = os.path.join(
182
- service.repo_path, DEVSERVICES_DIR_NAME, PROGRAMS_CONF_FILE_NAME
180
+ config_file_path = os.path.join(
181
+ service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
183
182
  )
184
-
185
183
  try:
186
- manager = SupervisorManager(programs_config_path, service_name=service.name)
184
+ manager = SupervisorManager(service.name, config_file_path)
187
185
  except SupervisorConfigError as e:
188
186
  capture_exception(e)
189
187
  return supervisor_logs
@@ -9,8 +9,8 @@ from argparse import Namespace
9
9
 
10
10
  from sentry_sdk import capture_exception
11
11
 
12
+ from devservices.constants import CONFIG_FILE_NAME
12
13
  from devservices.constants import DEVSERVICES_DIR_NAME
13
- from devservices.constants import PROGRAMS_CONF_FILE_NAME
14
14
  from devservices.exceptions import ConfigError
15
15
  from devservices.exceptions import ConfigNotFoundError
16
16
  from devservices.exceptions import SupervisorConfigError
@@ -46,16 +46,24 @@ def serve(args: Namespace) -> None:
46
46
  console.failure(str(e))
47
47
  exit(1)
48
48
 
49
- programs_config_path = os.path.join(
50
- service.repo_path, f"{DEVSERVICES_DIR_NAME}/{PROGRAMS_CONF_FILE_NAME}"
49
+ config_file_path = os.path.join(
50
+ service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
51
51
  )
52
- if not os.path.exists(programs_config_path):
53
- console.failure(f"No programs.conf file found in {programs_config_path}.")
52
+
53
+ try:
54
+ manager = SupervisorManager(service.name, config_file_path)
55
+ except SupervisorConfigError as e:
56
+ capture_exception(e, level="info")
57
+ console.failure(
58
+ f"Unable to bring up devserver due to supervisor config error: {str(e)}"
59
+ )
60
+ return
61
+
62
+ if not manager.has_programs:
63
+ console.failure(
64
+ "No programs found in config. Please add the devserver in the `x-programs` block to your config.yml"
65
+ )
54
66
  return
55
- manager = SupervisorManager(
56
- programs_config_path,
57
- service_name=service.name,
58
- )
59
67
 
60
68
  try:
61
69
  devserver_command = manager.get_program_command("devserver")
@@ -20,12 +20,12 @@ from devservices.constants import DependencyType
20
20
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
21
21
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
22
22
  from devservices.constants import DEVSERVICES_DIR_NAME
23
- from devservices.constants import PROGRAMS_CONF_FILE_NAME
24
23
  from devservices.exceptions import ConfigError
25
24
  from devservices.exceptions import ConfigNotFoundError
26
25
  from devservices.exceptions import DependencyError
27
26
  from devservices.exceptions import DockerComposeError
28
27
  from devservices.exceptions import ServiceNotFoundError
28
+ from devservices.exceptions import SupervisorConfigError
29
29
  from devservices.utils.console import Console
30
30
  from devservices.utils.dependencies import construct_dependency_graph
31
31
  from devservices.utils.dependencies import DependencyGraph
@@ -103,16 +103,18 @@ def status(args: Namespace) -> None:
103
103
  console.warning(f"Status unavailable. {service.name} is not running standalone")
104
104
  return # Since exit(0) is captured as an internal_error by sentry
105
105
 
106
- programs_config_path = os.path.join(
107
- service.repo_path, f"{DEVSERVICES_DIR_NAME}/{PROGRAMS_CONF_FILE_NAME}"
106
+ config_file_path = os.path.join(
107
+ service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
108
108
  )
109
109
  process_statuses = {}
110
- if os.path.exists(programs_config_path):
111
- supervisor_manager = SupervisorManager(
112
- programs_config_path,
113
- service.name,
114
- )
110
+
111
+ try:
112
+ supervisor_manager = SupervisorManager(service.name, config_file_path)
115
113
  process_statuses = supervisor_manager.get_all_process_info()
114
+ except SupervisorConfigError as e:
115
+ capture_exception(e)
116
+ console.failure(str(e))
117
+ exit(1)
116
118
 
117
119
  try:
118
120
  status_tree = get_status_for_service(service, process_statuses)
@@ -17,7 +17,6 @@ from devservices.constants import DependencyType
17
17
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR
18
18
  from devservices.constants import DEVSERVICES_DEPENDENCIES_CACHE_DIR_KEY
19
19
  from devservices.constants import DEVSERVICES_DIR_NAME
20
- from devservices.constants import PROGRAMS_CONF_FILE_NAME
21
20
  from devservices.exceptions import ConfigError
22
21
  from devservices.exceptions import ConfigNotFoundError
23
22
  from devservices.exceptions import ContainerHealthcheckFailedError
@@ -25,6 +24,7 @@ from devservices.exceptions import DependencyError
25
24
  from devservices.exceptions import DockerComposeError
26
25
  from devservices.exceptions import ModeDoesNotExistError
27
26
  from devservices.exceptions import ServiceNotFoundError
27
+ from devservices.exceptions import SupervisorConfigError
28
28
  from devservices.exceptions import SupervisorError
29
29
  from devservices.utils.console import Console
30
30
  from devservices.utils.console import Status
@@ -381,6 +381,14 @@ def bring_up_docker_compose_services(
381
381
  exit(1)
382
382
 
383
383
 
384
+ def _start_supervisor_program(
385
+ manager: SupervisorManager, program: str, status: Status
386
+ ) -> None:
387
+ """Start a single supervisor program."""
388
+ status.info(f"Starting {program}")
389
+ manager.start_process(program)
390
+
391
+
384
392
  def bring_up_supervisor_programs(
385
393
  service: Service, supervisor_programs: list[str], status: Status
386
394
  ) -> None:
@@ -391,18 +399,23 @@ def bring_up_supervisor_programs(
391
399
  f"Cannot bring up supervisor programs from outside the service repository. Please run the command from the service repository ({service.repo_path})"
392
400
  )
393
401
  return
394
- programs_config_path = os.path.join(
395
- service.repo_path, f"{DEVSERVICES_DIR_NAME}/{PROGRAMS_CONF_FILE_NAME}"
396
- )
397
402
 
398
- manager = SupervisorManager(
399
- programs_config_path,
400
- service_name=service.name,
403
+ config_file_path = os.path.join(
404
+ service.repo_path, DEVSERVICES_DIR_NAME, CONFIG_FILE_NAME
401
405
  )
402
406
 
407
+ try:
408
+ manager = SupervisorManager(service.name, config_file_path)
409
+ except SupervisorConfigError:
410
+ raise
411
+
403
412
  status.info("Starting supervisor daemon")
404
413
  manager.start_supervisor_daemon()
405
414
 
406
- for program in supervisor_programs:
407
- status.info(f"Starting {program}")
408
- manager.start_process(program)
415
+ with concurrent.futures.ThreadPoolExecutor() as executor:
416
+ futures = [
417
+ executor.submit(_start_supervisor_program, manager, program, status)
418
+ for program in supervisor_programs
419
+ ]
420
+ for future in concurrent.futures.as_completed(futures):
421
+ _ = future.result()
@@ -10,10 +10,10 @@ from supervisor.options import ServerOptions
10
10
  from devservices.constants import CONFIG_FILE_NAME
11
11
  from devservices.constants import DependencyType
12
12
  from devservices.constants import DEVSERVICES_DIR_NAME
13
- from devservices.constants import PROGRAMS_CONF_FILE_NAME
14
13
  from devservices.exceptions import ConfigNotFoundError
15
14
  from devservices.exceptions import ConfigParseError
16
15
  from devservices.exceptions import ConfigValidationError
16
+ from devservices.utils.supervisor import ProgramData
17
17
  from devservices.utils.supervisor import SupervisorManager
18
18
 
19
19
  VALID_VERSIONS = [0.1]
@@ -91,8 +91,10 @@ def load_service_config_from_file(repo_path: str) -> ServiceConfig:
91
91
 
92
92
  docker_compose_services = config.get("services", {}).keys()
93
93
 
94
- supervisor_programs = load_supervisor_programs_from_file(
95
- repo_path, service_config_data.get("service_name")
94
+ supervisor_programs = load_supervisor_programs_from_programs_data(
95
+ config_path,
96
+ service_config_data.get("service_name"),
97
+ config.get("x-programs", {}),
96
98
  )
97
99
 
98
100
  valid_dependency_keys = {field.name for field in fields(Dependency)}
@@ -113,7 +115,7 @@ def load_service_config_from_file(repo_path: str) -> ServiceConfig:
113
115
  dependency_type = DependencyType.COMPOSE
114
116
  else:
115
117
  raise ConfigValidationError(
116
- f"Dependency '{key}' is not remote but is not defined in docker-compose services or programs file"
118
+ f"Dependency '{key}' is not remote but is not defined in docker-compose services or x-programs"
117
119
  )
118
120
  else:
119
121
  dependency_type = DependencyType.SERVICE
@@ -142,13 +144,15 @@ def load_service_config_from_file(repo_path: str) -> ServiceConfig:
142
144
  return service_config
143
145
 
144
146
 
145
- def load_supervisor_programs_from_file(repo_path: str, service_name: str) -> set[str]:
146
- programs_config_path = os.path.join(
147
- repo_path, DEVSERVICES_DIR_NAME, PROGRAMS_CONF_FILE_NAME
148
- )
149
- if not os.path.exists(programs_config_path):
147
+ def load_supervisor_programs_from_programs_data(
148
+ service_config_path: str, service_name: str, programs_data: ProgramData
149
+ ) -> set[str]:
150
+ if not programs_data:
150
151
  return set()
151
- manager = SupervisorManager(programs_config_path, service_name=service_name)
152
+
153
+ manager = SupervisorManager(
154
+ service_name=service_name, service_config_path=service_config_path
155
+ )
152
156
  opts = ServerOptions()
153
157
  opts.configfile = manager.config_file_path
154
158
  opts.process_config()
@@ -25,7 +25,6 @@ class DependencyType(StrEnum):
25
25
  MINIMUM_DOCKER_COMPOSE_VERSION = "2.29.7"
26
26
  DEVSERVICES_DIR_NAME = "devservices"
27
27
  CONFIG_FILE_NAME = "config.yml"
28
- PROGRAMS_CONF_FILE_NAME = "programs.conf"
29
28
  DOCKER_CONFIG_DIR = os.environ.get("DOCKER_CONFIG", os.path.expanduser("~/.docker"))
30
29
  DOCKER_USER_PLUGIN_DIR = os.path.join(DOCKER_CONFIG_DIR, "cli-plugins/")
31
30
 
@@ -10,6 +10,7 @@ import xmlrpc.client
10
10
  from enum import IntEnum
11
11
  from typing import TypedDict
12
12
 
13
+ import yaml
13
14
  from sentry_sdk import capture_exception
14
15
  from supervisor.options import ServerOptions
15
16
 
@@ -91,24 +92,81 @@ class ProcessInfo(TypedDict):
91
92
  group: str
92
93
 
93
94
 
95
+ class SupervisorProgramConfig(TypedDict, total=False):
96
+ """Supervisor program configuration."""
97
+
98
+ command: str
99
+ autostart: str | bool
100
+ autorestart: str | bool
101
+ directory: str
102
+ environment: str
103
+ user: str
104
+ priority: str | int
105
+ startsecs: str | int
106
+ startretries: str | int
107
+ stdout_logfile: str
108
+ stderr_logfile: str
109
+ redirect_stderr: str | bool
110
+
111
+
112
+ ProgramData = dict[str, SupervisorProgramConfig]
113
+
114
+
115
+ # Default values for supervisor program configuration
116
+ SUPERVISOR_PROGRAM_DEFAULTS = {
117
+ "autostart": "false",
118
+ "autorestart": "true",
119
+ }
120
+
121
+
94
122
  class SupervisorManager:
95
- def __init__(self, config_file_path: str, service_name: str) -> None:
123
+ def __init__(
124
+ self,
125
+ service_name: str,
126
+ service_config_path: str,
127
+ ) -> None:
96
128
  self.service_name = service_name
97
- if not os.path.exists(config_file_path):
98
- raise SupervisorConfigError(
99
- f"Config file {config_file_path} does not exist"
100
- )
101
129
  self.socket_path = os.path.join(
102
130
  DEVSERVICES_SUPERVISOR_CONFIG_DIR, f"{service_name}.sock"
103
131
  )
104
- self.config_file_path = self._extend_config_file(config_file_path)
105
132
 
106
- def _extend_config_file(self, config_file_path: str) -> str:
107
- """Extend the supervisor config file passed into devservices with configuration settings that should be abstracted from users."""
133
+ # Load service config and extract x-programs data
134
+ if os.path.exists(service_config_path):
135
+ with open(service_config_path, "r", encoding="utf-8") as stream:
136
+ config = yaml.safe_load(stream)
137
+ else:
138
+ raise SupervisorConfigError(f"Config file {service_config_path} not found")
139
+
140
+ if config is None:
141
+ raise SupervisorConfigError(f"Config file {service_config_path} is empty")
108
142
 
143
+ programs_data: ProgramData = config.get("x-programs", {})
144
+
145
+ self.has_programs = len(programs_data.keys()) > 0
146
+
147
+ # Generate supervisor config file from x-programs data
148
+ self.config_file_path = self._generate_config_from_programs_data(programs_data)
149
+
150
+ def _generate_config_from_programs_data(self, programs_data: ProgramData) -> str:
109
151
  config = configparser.ConfigParser()
110
152
 
111
- config.read(config_file_path)
153
+ # Add program sections
154
+ for program_name, program_config in programs_data.items():
155
+ section_name = f"program:{program_name}"
156
+ config[section_name] = {}
157
+
158
+ # Apply defaults for any missing configuration values
159
+ program_config_with_defaults = {
160
+ **SUPERVISOR_PROGRAM_DEFAULTS,
161
+ **program_config,
162
+ }
163
+
164
+ for key, value in program_config_with_defaults.items():
165
+ if isinstance(value, bool):
166
+ config[section_name][key] = str(value).lower()
167
+ else:
168
+ config[section_name][key] = str(value)
169
+
112
170
  os.makedirs(DEVSERVICES_SUPERVISOR_CONFIG_DIR, exist_ok=True)
113
171
 
114
172
  # Set unix http server to use the socket path
@@ -129,13 +187,13 @@ class SupervisorManager:
129
187
  "supervisor.rpcinterface_factory": "supervisor.rpcinterface:make_main_rpcinterface"
130
188
  }
131
189
 
132
- extended_config_file_path = os.path.join(
190
+ config_file_path = os.path.join(
133
191
  DEVSERVICES_SUPERVISOR_CONFIG_DIR, f"{self.service_name}.processes.conf"
134
192
  )
135
- with open(extended_config_file_path, "w") as f:
193
+ with open(config_file_path, "w") as f:
136
194
  config.write(f)
137
195
 
138
- return extended_config_file_path
196
+ return config_file_path
139
197
 
140
198
  def _get_rpc_client(self) -> xmlrpc.client.ServerProxy:
141
199
  """Get or create an XML-RPC client that connects to the supervisor daemon."""
@@ -317,6 +375,9 @@ class SupervisorManager:
317
375
  def get_all_process_info(self) -> dict[str, ProcessInfo]:
318
376
  """Get status information for all supervisor programs."""
319
377
  # Check if supervisor client is up first, return empty list if down
378
+ if not self.has_programs:
379
+ return {}
380
+
320
381
  try:
321
382
  client = self._get_rpc_client()
322
383
  client.supervisor.getState()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devservices
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Requires-Python: >=3.11
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.2.0"
7
+ version = "1.2.1"
8
8
  # 3.11 is just for internal pypi compat
9
9
  requires-python = ">=3.11"
10
10
  dependencies = [
@@ -8,7 +8,6 @@ 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
12
11
 
13
12
  TESTING_DIR = os.path.abspath(os.path.dirname(__file__))
14
13
 
@@ -27,14 +26,6 @@ def create_config_file(
27
26
  yaml.dump(config, f, sort_keys=False, default_flow_style=False)
28
27
 
29
28
 
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
-
38
29
  def run_git_command(command: list[str], cwd: Path) -> None:
39
30
  subprocess.run(
40
31
  ["git", *command], cwd=cwd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
@@ -16,10 +16,8 @@ from devservices.configs.service_config import ServiceConfig
16
16
  from devservices.constants import CONFIG_FILE_NAME
17
17
  from devservices.constants import DependencyType
18
18
  from devservices.constants import DEVSERVICES_DIR_NAME
19
- from devservices.constants import PROGRAMS_CONF_FILE_NAME
20
19
  from devservices.exceptions import ConfigError
21
20
  from devservices.exceptions import ServiceNotFoundError
22
- from devservices.exceptions import SupervisorConfigError
23
21
  from devservices.exceptions import SupervisorError
24
22
  from devservices.utils.dependencies import install_and_verify_dependencies
25
23
  from devservices.utils.docker_compose import DockerComposeCommand
@@ -29,7 +27,6 @@ from devservices.utils.state import State
29
27
  from devservices.utils.state import StateTables
30
28
  from testing.utils import create_config_file
31
29
  from testing.utils import create_mock_git_repo
32
- from testing.utils import create_programs_conf_file
33
30
  from testing.utils import run_git_command
34
31
 
35
32
 
@@ -1367,6 +1364,11 @@ def test_down_supervisor_program_error(
1367
1364
  },
1368
1365
  "modes": {"default": ["supervisor-program"]},
1369
1366
  },
1367
+ "x-programs": {
1368
+ "supervisor-program": {
1369
+ "command": "echo 'Hello, world!'",
1370
+ }
1371
+ },
1370
1372
  "services": {
1371
1373
  "redis": {"image": "redis:6.2.14-alpine"},
1372
1374
  "clickhouse": {
@@ -1379,12 +1381,6 @@ def test_down_supervisor_program_error(
1379
1381
  create_config_file(service_path, config)
1380
1382
  os.chdir(service_path)
1381
1383
 
1382
- supervisor_program_config = """
1383
- [program:supervisor-program]
1384
- command=echo "Hello, world!"
1385
- """
1386
- create_programs_conf_file(service_path, supervisor_program_config)
1387
-
1388
1384
  args = Namespace(service_name=None, debug=False, exclude_local=False)
1389
1385
 
1390
1386
  with (
@@ -1432,6 +1428,11 @@ def test_down_supervisor_program_success(
1432
1428
  },
1433
1429
  "modes": {"default": ["supervisor-program"]},
1434
1430
  },
1431
+ "x-programs": {
1432
+ "supervisor-program": {
1433
+ "command": "echo 'Hello, world!'",
1434
+ }
1435
+ },
1435
1436
  "services": {
1436
1437
  "redis": {"image": "redis:6.2.14-alpine"},
1437
1438
  "clickhouse": {
@@ -1444,12 +1445,6 @@ def test_down_supervisor_program_success(
1444
1445
  create_config_file(service_path, config)
1445
1446
  os.chdir(service_path)
1446
1447
 
1447
- supervisor_program_config = """
1448
- [program:supervisor-program]
1449
- command=echo "Hello, world!"
1450
- """
1451
- create_programs_conf_file(service_path, supervisor_program_config)
1452
-
1453
1448
  args = Namespace(service_name=None, debug=False, exclude_local=False)
1454
1449
 
1455
1450
  with (
@@ -1478,42 +1473,6 @@ command=echo "Hello, world!"
1478
1473
  assert "example-service stopped" in captured.out.strip()
1479
1474
 
1480
1475
 
1481
- @mock.patch("devservices.utils.supervisor.SupervisorManager.stop_supervisor_daemon")
1482
- @mock.patch("devservices.utils.supervisor.SupervisorManager.stop_process")
1483
- def test_bring_down_supervisor_programs_no_programs_config(
1484
- mock_stop_process: mock.Mock,
1485
- mock_stop_supervisor_daemon: mock.Mock,
1486
- tmp_path: Path,
1487
- ) -> None:
1488
- service_config = ServiceConfig(
1489
- version=0.1,
1490
- service_name="test-service",
1491
- dependencies={
1492
- "supervisor-program": Dependency(
1493
- description="Supervisor program",
1494
- dependency_type=DependencyType.SUPERVISOR,
1495
- ),
1496
- },
1497
- modes={"default": ["supervisor-program"]},
1498
- )
1499
- service = Service(
1500
- name="test-service",
1501
- repo_path=str(tmp_path),
1502
- config=service_config,
1503
- )
1504
-
1505
- status = mock.MagicMock()
1506
-
1507
- with pytest.raises(
1508
- SupervisorConfigError,
1509
- match=f"Config file {tmp_path / DEVSERVICES_DIR_NAME / PROGRAMS_CONF_FILE_NAME} does not exist",
1510
- ):
1511
- bring_down_supervisor_programs(["supervisor-program"], service, status)
1512
-
1513
- mock_stop_supervisor_daemon.assert_not_called()
1514
- mock_stop_process.assert_not_called()
1515
-
1516
-
1517
1476
  @mock.patch("devservices.utils.supervisor.SupervisorManager.stop_supervisor_daemon")
1518
1477
  @mock.patch("devservices.utils.supervisor.SupervisorManager.stop_process")
1519
1478
  def test_bring_down_supervisor_programs_empty_list(
@@ -1573,14 +1532,20 @@ def test_bring_down_supervisor_programs_success(
1573
1532
  config=service_config,
1574
1533
  )
1575
1534
 
1576
- programs_conf_path = tmp_path / DEVSERVICES_DIR_NAME / PROGRAMS_CONF_FILE_NAME
1577
-
1578
- create_programs_conf_file(
1579
- programs_conf_path,
1580
- """
1581
- [program:supervisor-program]
1582
- command=echo "Hello, world!"
1583
- """,
1535
+ create_config_file(
1536
+ tmp_path,
1537
+ {
1538
+ "x-sentry-service-config": {
1539
+ "version": 0.1,
1540
+ "service_name": "test-service",
1541
+ },
1542
+ "x-programs": {
1543
+ "supervisor-program": {
1544
+ "command": "echo 'Hello, world!'",
1545
+ }
1546
+ },
1547
+ "services": {},
1548
+ },
1584
1549
  )
1585
1550
 
1586
1551
  status = mock.MagicMock()