uber-compose 2.3.3__tar.gz → 3.0.0a1__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 (75) hide show
  1. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/PKG-INFO +2 -1
  2. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/README.md +1 -0
  3. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/__init__.py +22 -1
  4. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/constants.py +11 -4
  5. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/docker_compose.py +10 -0
  6. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/sequence_run_types.py +2 -0
  7. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/system_docker_compose.py +3 -0
  8. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/utils/compose_instance_cfg.py +9 -2
  9. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/utils/state_waiting.py +6 -2
  10. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/errors/up.py +4 -0
  11. uber_compose-3.0.0a1/uber_compose/helpers/broken_services.py +47 -0
  12. uber_compose-3.0.0a1/uber_compose/helpers/current_env.py +20 -0
  13. uber_compose-3.0.0a1/uber_compose/helpers/env_id.py +21 -0
  14. uber_compose-3.0.0a1/uber_compose/helpers/hooks.py +24 -0
  15. uber_compose-3.0.0a1/uber_compose/parallel_plugin/__init__.py +39 -0
  16. uber_compose-3.0.0a1/uber_compose/parallel_plugin/grouper.py +97 -0
  17. uber_compose-3.0.0a1/uber_compose/parallel_plugin/grouping.py +30 -0
  18. uber_compose-3.0.0a1/uber_compose/parallel_plugin/plugin.py +149 -0
  19. uber_compose-3.0.0a1/uber_compose/parallel_plugin/runner.py +205 -0
  20. uber_compose-3.0.0a1/uber_compose/parallel_plugin/types.py +34 -0
  21. uber_compose-3.0.0a1/uber_compose/parallel_plugin/worker_api.py +124 -0
  22. uber_compose-3.0.0a1/uber_compose/parallel_plugin/worker_filter.py +38 -0
  23. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/uber_compose.py +225 -5
  24. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/vedro_plugin/base_structures/common_json_cli.py +18 -8
  25. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/vedro_plugin/helpers/scenario_tag_processing.py +6 -6
  26. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/vedro_plugin/plugin.py +91 -2
  27. uber_compose-3.0.0a1/uber_compose/version +1 -0
  28. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose.egg-info/PKG-INFO +2 -1
  29. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose.egg-info/SOURCES.txt +11 -0
  30. uber_compose-2.3.3/uber_compose/helpers/broken_services.py +0 -29
  31. uber_compose-2.3.3/uber_compose/version +0 -1
  32. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/LICENSE +0 -0
  33. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/setup.cfg +0 -0
  34. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/setup.py +0 -0
  35. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/__init__.py +0 -0
  36. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/docker_compose_shell/__init__.py +0 -0
  37. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/docker_compose_shell/interface.py +0 -0
  38. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/docker_compose_shell/types.py +0 -0
  39. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/utils/__init__.py +0 -0
  40. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/utils/compose_files.py +0 -0
  41. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/utils/env_files.py +0 -0
  42. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/utils/process_command_output.py +0 -0
  43. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/core/utils/shell_process.py +0 -0
  44. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/env_description/__init__.py +0 -0
  45. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/env_description/env_tools.py +0 -0
  46. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/env_description/env_types.py +0 -0
  47. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/errors/__init__.py +0 -0
  48. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/errors/migrations.py +0 -0
  49. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/helpers/__init__.py +0 -0
  50. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/helpers/bytes_pickle.py +0 -0
  51. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/helpers/countdown_counter.py +0 -0
  52. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/helpers/exec_record.py +0 -0
  53. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/helpers/exec_result.py +0 -0
  54. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/helpers/health_policy.py +0 -0
  55. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/helpers/jobs_result.py +0 -0
  56. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/helpers/labels.py +0 -0
  57. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/helpers/singleton.py +0 -0
  58. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/helpers/state_keeper.py +0 -0
  59. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/output/__init__.py +0 -0
  60. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/output/console.py +0 -0
  61. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/output/styles.py +0 -0
  62. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/utils/__init__.py +0 -0
  63. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/utils/docker_compose_files_path.py +0 -0
  64. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/utils/docker_compose_service_deps.py +0 -0
  65. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/utils/search_docker_compose_files.py +0 -0
  66. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/utils/services_construction.py +0 -0
  67. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/vedro_plugin/__init__.py +0 -0
  68. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/vedro_plugin/base_structures/__init__.py +0 -0
  69. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/vedro_plugin/helpers/__init__.py +0 -0
  70. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/vedro_plugin/helpers/scenario_ordering.py +0 -0
  71. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/vedro_plugin/helpers/test_env_vars_setter.py +0 -0
  72. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose/version.py +0 -0
  73. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose.egg-info/dependency_links.txt +0 -0
  74. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose.egg-info/requires.txt +0 -0
  75. {uber_compose-2.3.3 → uber_compose-3.0.0a1}/uber_compose.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: uber-compose
3
- Version: 2.3.3
3
+ Version: 3.0.0a1
4
4
  Summary: docker compose testing env orchestrator
5
5
  Home-page: https://github.com/ko10ok/uber-compose
6
6
  Author: Yuriy Sagitov
@@ -231,6 +231,7 @@ This is particularly useful when:
231
231
  - **[E2E Test Setup](docs/E2E_CONTAINER_SETUP.md)** - Configure test container for E2E testing
232
232
  - **[CLI Usage Guide](docs/CLI_USAGE.md)** - Guide for using CommonJsonCli with JSON log parsing
233
233
  - **[X-Migration](docs/X_MIGRATION.md)** - Extended service initialization and migration commands
234
+ - **[Lifecycle Hooks](docs/HOOKS.md)** - Register callbacks that run after the environment is up
234
235
 
235
236
  ## 🛠️ Development Guide
236
237
 
@@ -198,6 +198,7 @@ This is particularly useful when:
198
198
  - **[E2E Test Setup](docs/E2E_CONTAINER_SETUP.md)** - Configure test container for E2E testing
199
199
  - **[CLI Usage Guide](docs/CLI_USAGE.md)** - Guide for using CommonJsonCli with JSON log parsing
200
200
  - **[X-Migration](docs/X_MIGRATION.md)** - Extended service initialization and migration commands
201
+ - **[Lifecycle Hooks](docs/HOOKS.md)** - Register callbacks that run after the environment is up
201
202
 
202
203
  ## 🛠️ Development Guide
203
204
 
@@ -10,6 +10,23 @@ from uber_compose.env_description.env_types import OverridenService
10
10
  from uber_compose.env_description.env_types import Service
11
11
  from uber_compose.helpers.exec_result import ExecTimeout
12
12
  from uber_compose.helpers.health_policy import UpHealthPolicy
13
+ from uber_compose.helpers.hooks import UberComposeEvent
14
+ from uber_compose.parallel_plugin import ExecutionPlan
15
+ from uber_compose.parallel_plugin import VedroUberComposeParallel
16
+ from uber_compose.parallel_plugin import WorkerAssignment
17
+ from uber_compose.parallel_plugin import build_execution_plan
18
+ from uber_compose.parallel_plugin import build_execution_plan_from_scheduler
19
+ from uber_compose.parallel_plugin import env_group_key
20
+ from uber_compose.parallel_plugin import env_group_key_for
21
+ from uber_compose.parallel_plugin import get_execution_plan
22
+ from uber_compose.parallel_plugin import get_namespace_map
23
+ from uber_compose.parallel_plugin import get_worker_groups
24
+ from uber_compose.parallel_plugin import get_worker_id
25
+ from uber_compose.parallel_plugin import get_workers_total
26
+ from uber_compose.parallel_plugin import is_parallel_worker
27
+ from uber_compose.parallel_plugin import is_parallel_worker_process
28
+ from uber_compose.parallel_plugin import reset_execution_plan
29
+ from uber_compose.parallel_plugin import set_execution_plan
13
30
  from uber_compose.uber_compose import SystemUberCompose
14
31
  from uber_compose.uber_compose import TheUberCompose
15
32
  from uber_compose.vedro_plugin.base_structures.common_json_cli import CommandResult
@@ -27,5 +44,9 @@ __all__ = (
27
44
  'CommonJsonCli', 'CommandResult', 'ProcessExit', 'JsonParser', 'json_parser', 'ExecTimeout', 'TimeOutCheck',
28
45
  'ExecLifeCyclePolicy',
29
46
  'VedroUberCompose', 'UpHealthPolicy', 'DEFAULT_COMPOSE', 'ComposeConfig', 'DEFAULT_ENV_DESCRIPTION',
30
- 'DEFAULT_ENV_ID',
47
+ 'DEFAULT_ENV_ID', 'UberComposeEvent',
48
+ 'ExecutionPlan', 'WorkerAssignment', 'env_group_key', 'env_group_key_for',
49
+ 'get_execution_plan', 'get_namespace_map', 'get_worker_groups', 'get_worker_id', 'get_workers_total',
50
+ 'is_parallel_worker', 'is_parallel_worker_process', 'reset_execution_plan', 'set_execution_plan',
51
+ 'VedroUberComposeParallel', 'build_execution_plan', 'build_execution_plan_from_scheduler',
31
52
  )
@@ -6,11 +6,14 @@ class Constants:
6
6
  def __init__(self,
7
7
  docker_compose_files_scan_depth: int = 2,
8
8
  cli_compose_util_override: str = None,
9
+ compose_project_name: str = None,
10
+ tmp_envs_path: str | Path = None,
11
+ in_docker_project_root_path: str | Path = None,
9
12
  ):
10
13
  self.default_log_policy = os.environ.get('LOG_POLICY', 'DEFAULT')
11
14
 
12
- self.project: str = os.environ.get('COMPOSE_PROJECT_NAME')
13
- self.compose_project_name = os.environ.get('COMPOSE_PROJECT_NAME')
15
+ self.project: str = compose_project_name or os.environ.get('COMPOSE_PROJECT_NAME')
16
+ self.compose_project_name = self.project
14
17
  assert self.compose_project_name, (
15
18
  'COMPOSE_PROJECT_NAME environment variable is not set: - COMPOSE_PROJECT_NAME=${PWD##*/}'
16
19
  )
@@ -19,8 +22,12 @@ class Constants:
19
22
 
20
23
  # inner directories
21
24
  self.non_stop_containers: list[str] = os.environ.get('NON_STOP_CONTAINERS', 'e2e,dockersock').split(',')
22
- self.tmp_envs_path: Path = Path(os.environ.get('TMP_ENVS_DIRECTORY', '/tmp/uc-envs'))
23
- self.in_docker_project_root_path: Path = Path(os.environ.get('PROJECT_ROOT_DIRECTORY', '/project'))
25
+ self.tmp_envs_path: Path = Path(
26
+ tmp_envs_path or os.environ.get('TMP_ENVS_DIRECTORY', '/tmp/uc-envs')
27
+ )
28
+ self.in_docker_project_root_path: Path = Path(
29
+ in_docker_project_root_path or os.environ.get('PROJECT_ROOT_DIRECTORY', '/project')
30
+ )
24
31
 
25
32
  self.host_project_root_directory: Path = Path(
26
33
  os.environ.get('HOST_PROJECT_ROOT_DIRECTORY', '__host_project_root__')
@@ -17,6 +17,7 @@ from uber_compose.env_description.env_types import EventStage
17
17
  from uber_compose.errors.up import ServicesUpError
18
18
  from uber_compose.helpers.health_policy import UpHealthPolicy
19
19
  from uber_compose.helpers.jobs_result import JobResult
20
+ from uber_compose.helpers.labels import Label
20
21
  from uber_compose.output.console import Logger
21
22
  from uber_compose.output.styles import Style
22
23
 
@@ -206,6 +207,7 @@ class ComposeInstance:
206
207
  delay_s=self.health_policy.service_up_check_delay_s,
207
208
  logger_func=self.logger.stage_details,
208
209
  get_compose_state=self.compose_executor.dc_state,
210
+ service_filter=self._is_service_in_this_namespace,
209
211
  )
210
212
  if check_up_result != JobResult.GOOD:
211
213
  services_status = await self.compose_executor.dc_state()
@@ -222,6 +224,12 @@ class ComposeInstance:
222
224
  migrations
223
225
  )
224
226
 
227
+ def _is_service_in_this_namespace(self, service_state) -> bool:
228
+ return (
229
+ service_state.labels.get(Label.ENV_ID)
230
+ == self.compose_instance_files.env_config_instance.env_id
231
+ )
232
+
225
233
  async def run(self):
226
234
  self.compose_instance_files = await self.generate_config_files()
227
235
 
@@ -242,6 +250,7 @@ class ComposeInstance:
242
250
  service
243
251
  for service_tier_pack in services_tiers
244
252
  for service in service_tier_pack
253
+ if service not in self.except_containers
245
254
  ]
246
255
 
247
256
  await self.run_migration(
@@ -267,6 +276,7 @@ class ComposeInstance:
267
276
  delay_s=self.health_policy.service_up_check_delay_s,
268
277
  logger_func=self.logger.stage,
269
278
  get_compose_state=self.compose_executor.dc_state,
279
+ service_filter=self._is_service_in_this_namespace,
270
280
  )
271
281
  if check_up_result != JobResult.GOOD:
272
282
  services_status = await self.compose_executor.dc_state()
@@ -12,6 +12,8 @@ class ComposeConfig(NamedTuple):
12
12
  compose_files: str
13
13
  overridden_services: list[OverridenService] = None
14
14
  parallel_env_limit: Union[int] = 1
15
+ env_parallelism: dict[str, int] | None = None
16
+ default_env_parallelism: int = 1
15
17
 
16
18
 
17
19
  class EnvInstanceConfig(NamedTuple):
@@ -107,5 +107,8 @@ class SystemDockerCompose:
107
107
  services_state = await self.dc_shell.dc_state()
108
108
  return [service.name for service in services_state.get_all_for()]
109
109
 
110
+ async def get_all_services_state(self) -> ServicesComposeState:
111
+ return await self.dc_shell.dc_state()
112
+
110
113
  async def down_services(self, services: list[str] | None = None) -> None:
111
114
  await self.dc_shell.dc_down(services=services)
@@ -7,6 +7,8 @@ from uber_compose.env_description.env_types import Env
7
7
  from uber_compose.env_description.env_types import Environment
8
8
  from uber_compose.env_description.env_types import Service
9
9
  from uber_compose.env_description.env_types import ServiceMode
10
+ from uber_compose.helpers.env_id import get_stable_env_id as get_stable_env_id # noqa: F401
11
+ from uber_compose.helpers.env_id import get_template_hash as get_template_hash # noqa: F401
10
12
 
11
13
 
12
14
  def make_env_service_name(service, env_id):
@@ -52,8 +54,13 @@ def get_new_env_id() -> str:
52
54
 
53
55
  def get_service_map(env: Environment, new_env_id: str):
54
56
  return {
55
- service_name: make_env_service_name(service_name, new_env_id)
56
- for service_name, service in env.get_services().items() if service.mode != ServiceMode.OFF
57
+ service_name: (
58
+ service_name
59
+ if service.mode in (ServiceMode.OFF, ServiceMode.SINGLETON)
60
+ else make_env_service_name(service_name, new_env_id)
61
+ )
62
+ for service_name, service in env.get_services().items()
63
+ if service.mode != ServiceMode.OFF
57
64
  }
58
65
 
59
66
 
@@ -23,10 +23,14 @@ def is_service_not_running_or_not_healthy(service_state: ServiceComposeState) ->
23
23
 
24
24
 
25
25
  async def wait_all_services_up(attempts, delay_s, logger_func: Callable, get_compose_state,
26
- is_service_in_bad_state=is_service_not_running_or_not_healthy) -> JobResult:
26
+ is_service_in_bad_state=is_service_not_running_or_not_healthy,
27
+ service_filter: Callable = None) -> JobResult:
27
28
  counter_keeper = CountdownCounterKeeper(attempts)
28
29
  state_keeper = StateKeeper()
29
30
 
31
+ if service_filter is None:
32
+ service_filter = lambda service_state: True
33
+
30
34
  async def check_once() -> JobResult:
31
35
  output_style = Style()
32
36
 
@@ -34,7 +38,7 @@ async def wait_all_services_up(attempts, delay_s, logger_func: Callable, get_com
34
38
  logger_func(Text('Starting services check up', style=output_style.info))
35
39
  state_keeper.update_state(ServicesState.DEFAULT_STATE)
36
40
 
37
- services_state = await get_compose_state()
41
+ services_state = (await get_compose_state()).get_all_for(service_filter)
38
42
  non_ready_services = [
39
43
  service
40
44
  for service in services_state
@@ -5,3 +5,7 @@ class ServicesUpError(Exception):
5
5
 
6
6
  def __str__(self):
7
7
  return self.message
8
+
9
+
10
+ class SingletonConfigChangedError(ServicesUpError):
11
+ ...
@@ -0,0 +1,47 @@
1
+ from uber_compose import Environment
2
+ from uber_compose.core.docker_compose_shell.types import ServicesComposeState
3
+ from uber_compose.core.utils.compose_instance_cfg import get_service_map
4
+ from uber_compose.core.utils.state_waiting import is_service_not_running_or_not_healthy
5
+ from uber_compose.helpers.labels import Label
6
+
7
+
8
+ def calc_broken_services(
9
+ services_state: ServicesComposeState,
10
+ config_template: Environment,
11
+ excluded_from_check: list[str] = None,
12
+ env_id: str = None,
13
+ ) -> list[str]:
14
+ if excluded_from_check is None:
15
+ excluded_from_check = []
16
+
17
+ if env_id is None and services_state.get_any() is not None:
18
+ env_id = services_state.get_any().labels.get(Label.ENV_ID)
19
+
20
+ if env_id is not None:
21
+ expected_services_map = get_service_map(config_template, env_id)
22
+ else:
23
+ expected_services_map = {
24
+ service_name: service_name
25
+ for service_name in config_template.get_services_names()
26
+ }
27
+
28
+ non_ready_services = [
29
+ service.name
30
+ for service in services_state
31
+ if is_service_not_running_or_not_healthy(service)
32
+ ]
33
+ not_included_services = [
34
+ service
35
+ for service in expected_services_map.values()
36
+ if service not in services_state.get_services_names()
37
+ ]
38
+ skipped_overridden_services = [
39
+ expected_services_map.get(service.service.name, service.service.name)
40
+ for service in config_template.get_overridden_services()
41
+ if expected_services_map.get(service.service.name, service.service.name)
42
+ not in services_state.get_services_names()
43
+ ]
44
+ return list(
45
+ set(non_ready_services) | set(not_included_services)
46
+ - set(excluded_from_check) - set(skipped_overridden_services)
47
+ )
@@ -0,0 +1,20 @@
1
+ from contextvars import ContextVar
2
+
3
+ from uber_compose.core.sequence_run_types import DEFAULT_ENV_ID
4
+
5
+ _current_env_id: ContextVar[str] = ContextVar(
6
+ 'uber_compose_current_env_id',
7
+ default=DEFAULT_ENV_ID,
8
+ )
9
+
10
+
11
+ def set_current_env_id(env_id: str) -> None:
12
+ _current_env_id.set(env_id)
13
+
14
+
15
+ def get_current_env_id() -> str:
16
+ return _current_env_id.get()
17
+
18
+
19
+ def reset_current_env_id() -> None:
20
+ _current_env_id.set(DEFAULT_ENV_ID)
@@ -0,0 +1,21 @@
1
+ import hashlib
2
+
3
+ from uber_compose.helpers.bytes_pickle import base64_pickled
4
+
5
+
6
+ def get_template_hash(config_template, length: int = 4) -> str:
7
+ payload = base64_pickled(config_template)
8
+ return hashlib.sha1(payload.encode()).hexdigest()[:length]
9
+
10
+
11
+ def get_stable_env_id(config_template, index: int) -> str:
12
+ return f'w{index}-{get_template_hash(config_template)}'
13
+
14
+
15
+ def stable_index(key: str, total: int) -> int:
16
+ total = max(total, 1)
17
+ digest = hashlib.sha1(str(key).encode()).hexdigest()
18
+ return int(digest, 16) % total
19
+
20
+
21
+ __all__ = ("get_stable_env_id", "get_template_hash", "stable_index",)
@@ -0,0 +1,24 @@
1
+ from collections import defaultdict
2
+ from enum import Enum
3
+ import inspect
4
+ from typing import Any
5
+ from typing import Callable
6
+
7
+
8
+ class UberComposeEvent(Enum):
9
+ AFTER_UP = 'after_up'
10
+ AFTER_UPDATED_ENV_UP = 'after_updated_env_up'
11
+
12
+
13
+ class HooksRegistry:
14
+ def __init__(self) -> None:
15
+ self._hooks: dict[UberComposeEvent, list[tuple[Callable[..., Any], tuple, dict]]] = defaultdict(list)
16
+
17
+ def on(self, event: UberComposeEvent, func: Callable[..., Any], *args: Any, **kwargs: Any) -> None:
18
+ self._hooks[event].append((func, args, kwargs))
19
+
20
+ async def run(self, event: UberComposeEvent) -> None:
21
+ for func, args, kwargs in self._hooks[event]:
22
+ result = func(*args, **kwargs)
23
+ if inspect.isawaitable(result):
24
+ await result
@@ -0,0 +1,39 @@
1
+ from uber_compose.parallel_plugin.grouper import assign_groups_to_workers
2
+ from uber_compose.parallel_plugin.grouper import build_execution_plan
3
+ from uber_compose.parallel_plugin.grouper import build_execution_plan_from_scheduler
4
+ from uber_compose.parallel_plugin.grouper import build_replica_execution_plan
5
+ from uber_compose.parallel_plugin.grouper import collect_group_counts
6
+ from uber_compose.parallel_plugin.grouping import env_group_key
7
+ from uber_compose.parallel_plugin.grouping import env_group_key_for
8
+ from uber_compose.parallel_plugin.grouping import scenario_env
9
+ from uber_compose.parallel_plugin.plugin import VedroUberComposeParallel
10
+ from uber_compose.parallel_plugin.plugin import VedroUberComposeParallelPlugin
11
+ from uber_compose.parallel_plugin.types import ExecutionPlan
12
+ from uber_compose.parallel_plugin.types import WorkerAssignment
13
+ from uber_compose.parallel_plugin.worker_api import get_env_parallelism
14
+ from uber_compose.parallel_plugin.worker_api import get_env_replicas_total
15
+ from uber_compose.parallel_plugin.worker_api import get_execution_plan
16
+ from uber_compose.parallel_plugin.worker_api import get_namespace_map
17
+ from uber_compose.parallel_plugin.worker_api import get_worker_groups
18
+ from uber_compose.parallel_plugin.worker_api import get_worker_id
19
+ from uber_compose.parallel_plugin.worker_api import get_worker_replica_index
20
+ from uber_compose.parallel_plugin.worker_api import get_workers_total
21
+ from uber_compose.parallel_plugin.worker_api import is_parallel_worker
22
+ from uber_compose.parallel_plugin.worker_api import is_parallel_worker_process
23
+ from uber_compose.parallel_plugin.worker_api import reset_env_parallelism
24
+ from uber_compose.parallel_plugin.worker_api import reset_execution_plan
25
+ from uber_compose.parallel_plugin.worker_api import set_env_parallelism
26
+ from uber_compose.parallel_plugin.worker_api import set_execution_plan
27
+ from uber_compose.parallel_plugin.worker_filter import ignore_foreign_scenarios
28
+
29
+ __all__ = (
30
+ 'ExecutionPlan', 'WorkerAssignment',
31
+ 'VedroUberComposeParallel', 'VedroUberComposeParallelPlugin',
32
+ 'assign_groups_to_workers', 'build_execution_plan', 'build_execution_plan_from_scheduler',
33
+ 'build_replica_execution_plan', 'collect_group_counts', 'env_group_key', 'env_group_key_for',
34
+ 'get_env_parallelism', 'get_env_replicas_total', 'get_execution_plan', 'get_namespace_map',
35
+ 'get_worker_groups', 'get_worker_id', 'get_worker_replica_index', 'get_workers_total',
36
+ 'ignore_foreign_scenarios', 'is_parallel_worker', 'is_parallel_worker_process',
37
+ 'reset_env_parallelism', 'reset_execution_plan', 'scenario_env',
38
+ 'set_env_parallelism', 'set_execution_plan',
39
+ )
@@ -0,0 +1,97 @@
1
+ from collections import defaultdict
2
+ from typing import Callable
3
+
4
+ from vedro.core import ScenarioScheduler
5
+ from vedro.core import VirtualScenario
6
+
7
+ from uber_compose.helpers.env_id import get_stable_env_id
8
+ from uber_compose.parallel_plugin.types import ExecutionPlan
9
+ from uber_compose.parallel_plugin.types import WorkerAssignment
10
+
11
+
12
+ async def collect_group_counts(
13
+ scenarios: ScenarioScheduler,
14
+ grouping_key: Callable[[VirtualScenario], str],
15
+ ) -> dict[str, int]:
16
+ counts: dict[str, int] = defaultdict(int)
17
+ async for scenario in scenarios:
18
+ if scenario.is_skipped():
19
+ continue
20
+ counts[grouping_key(scenario)] += 1
21
+ return dict(counts)
22
+
23
+
24
+ def assign_groups_to_workers(group_counts: dict[str, int], workers_total: int) -> list[list[str]]:
25
+ workers_total = max(workers_total, 1)
26
+ workers: list[list[str]] = [[] for _ in range(workers_total)]
27
+ worker_loads = [0] * workers_total
28
+
29
+ groups = sorted(group_counts.items(), key=lambda item: (-item[1], item[0]))
30
+ for group_key, count in groups:
31
+ worker_id = min(range(workers_total), key=lambda index: (worker_loads[index], index))
32
+ workers[worker_id].append(group_key)
33
+ worker_loads[worker_id] += count
34
+
35
+ return workers
36
+
37
+
38
+ def build_execution_plan(
39
+ group_counts: dict[str, int],
40
+ workers_total: int,
41
+ namespace_map: dict[str, str] | None = None,
42
+ ) -> ExecutionPlan:
43
+ namespace_map = namespace_map or {}
44
+ workers = assign_groups_to_workers(group_counts, workers_total)
45
+ assignments = tuple(
46
+ WorkerAssignment(
47
+ worker_id=worker_id,
48
+ groups=tuple(sorted(groups)),
49
+ namespace_map={group: namespace_map[group] for group in groups if group in namespace_map},
50
+ )
51
+ for worker_id, groups in enumerate(workers)
52
+ )
53
+ return ExecutionPlan(workers_total=max(workers_total, 1), assignments=assignments)
54
+
55
+
56
+ async def build_execution_plan_from_scheduler(
57
+ scenarios: ScenarioScheduler,
58
+ workers_total: int,
59
+ grouping_key: Callable[[VirtualScenario], str],
60
+ namespace_map: dict[str, str] | None = None,
61
+ ) -> ExecutionPlan:
62
+ group_counts = await collect_group_counts(scenarios, grouping_key)
63
+ return build_execution_plan(group_counts, workers_total, namespace_map)
64
+
65
+
66
+ def build_replica_execution_plan(
67
+ group_counts: dict[str, int],
68
+ group_envs: dict[str, object],
69
+ namespace_counts: dict[str, int],
70
+ default_replicas: int = 1,
71
+ ) -> ExecutionPlan:
72
+ assignments = []
73
+ worker_id = 0
74
+ for group_key in sorted(group_envs):
75
+ env = group_envs[group_key]
76
+ replicas = max(int(namespace_counts.get(group_key, default_replicas)), 1)
77
+ replicas = min(replicas, max(group_counts.get(group_key, 1), 1))
78
+ for replica_index in range(replicas):
79
+ assignments.append(WorkerAssignment(
80
+ worker_id=worker_id,
81
+ groups=(group_key,),
82
+ namespace_map={group_key: get_stable_env_id(env, replica_index)},
83
+ replica_index=replica_index,
84
+ replicas_total=replicas,
85
+ ))
86
+ worker_id += 1
87
+ return ExecutionPlan(
88
+ workers_total=worker_id,
89
+ assignments=tuple(assignments),
90
+ env_templates=dict(group_envs),
91
+ )
92
+
93
+
94
+ __all__ = (
95
+ 'assign_groups_to_workers', 'build_execution_plan', 'build_execution_plan_from_scheduler',
96
+ 'build_replica_execution_plan', 'collect_group_counts',
97
+ )
@@ -0,0 +1,30 @@
1
+ from vedro.core import VirtualScenario
2
+
3
+ from uber_compose.env_description.env_types import DEFAULT_ENV_DESCRIPTION
4
+ from uber_compose.env_description.env_types import Environment
5
+ from uber_compose.helpers.bytes_pickle import base64_pickled
6
+
7
+
8
+ def scenario_env(scenario: VirtualScenario) -> Environment | None:
9
+ original = getattr(scenario, '_orig_scenario', scenario)
10
+ env = None
11
+ if hasattr(original, 'tags'):
12
+ for tag in original.tags:
13
+ if isinstance(tag, Environment):
14
+ env = tag
15
+ if hasattr(original, 'env'):
16
+ env = original.env
17
+ return env
18
+
19
+
20
+ def env_group_key_for(env: Environment | None) -> str:
21
+ if env is None or env.description == DEFAULT_ENV_DESCRIPTION:
22
+ env = None
23
+ return base64_pickled(env)
24
+
25
+
26
+ def env_group_key(scenario: VirtualScenario) -> str:
27
+ return env_group_key_for(scenario_env(scenario))
28
+
29
+
30
+ __all__ = ("env_group_key", "env_group_key_for", "scenario_env",)
@@ -0,0 +1,149 @@
1
+ from typing import Type
2
+
3
+ from vedro.core import ConfigType
4
+ from vedro.core import Dispatcher
5
+ from vedro.core import Plugin
6
+ from vedro.core import PluginConfig
7
+ from vedro.events import ArgParseEvent
8
+ from vedro.events import ArgParsedEvent
9
+ from vedro.events import ConfigLoadedEvent
10
+ from vedro.events import StartupEvent
11
+
12
+ from uber_compose.env_description.env_types import DEFAULT_ENV_DESCRIPTION
13
+ from uber_compose.parallel_plugin.grouper import build_execution_plan_from_scheduler
14
+ from uber_compose.parallel_plugin.grouper import build_replica_execution_plan
15
+ from uber_compose.parallel_plugin.grouper import collect_group_counts
16
+ from uber_compose.parallel_plugin.grouping import env_group_key
17
+ from uber_compose.parallel_plugin.grouping import env_group_key_for
18
+ from uber_compose.parallel_plugin.grouping import scenario_env
19
+ from uber_compose.parallel_plugin.runner import ParallelScenarioRunner
20
+ from uber_compose.parallel_plugin.worker_api import get_env_parallelism
21
+ from uber_compose.parallel_plugin.worker_api import get_env_replicas_total
22
+ from uber_compose.parallel_plugin.worker_api import get_worker_groups
23
+ from uber_compose.parallel_plugin.worker_api import get_worker_replica_index
24
+ from uber_compose.parallel_plugin.worker_api import is_parallel_worker_process
25
+ from uber_compose.parallel_plugin.worker_api import set_execution_plan
26
+ from uber_compose.parallel_plugin.worker_filter import ignore_foreign_scenarios
27
+
28
+
29
+ class VedroUberComposeParallelPlugin(Plugin):
30
+ def __init__(self, config: Type["VedroUberComposeParallel"]) -> None:
31
+ super().__init__(config)
32
+ self._enabled = config.enabled
33
+ self._grouping_key = config.grouping_key or env_group_key
34
+ self._parallel = max(int(config.parallel or 1), 1)
35
+ self._global_config: ConfigType | None = None
36
+ self._runner_registered = False
37
+ self._refresh_worker_state()
38
+
39
+ def _refresh_worker_state(self) -> None:
40
+ self._is_worker = is_parallel_worker_process()
41
+ self._worker_groups = get_worker_groups()
42
+ self._worker_replica_index = get_worker_replica_index()
43
+ self._env_replicas_total = get_env_replicas_total()
44
+
45
+ def subscribe(self, dispatcher: Dispatcher) -> None:
46
+ if not self._enabled:
47
+ return
48
+
49
+ dispatcher.listen(ConfigLoadedEvent, self.on_config_loaded) \
50
+ .listen(ArgParseEvent, self.handle_arg_parse, priority=-100) \
51
+ .listen(ArgParsedEvent, self.handle_arg_parsed, priority=-100) \
52
+ .listen(StartupEvent, self.handle_prepare_scenarios, priority=-100)
53
+
54
+ def on_config_loaded(self, event: ConfigLoadedEvent) -> None:
55
+ self._global_config = event.config
56
+
57
+ def handle_arg_parse(self, event: ArgParseEvent) -> None:
58
+ group = event.arg_parser.add_argument_group("Uber Compose Parallel")
59
+ group.add_argument("--uc-parallel",
60
+ type=int,
61
+ default=None,
62
+ help="Number of parallel workers (1 keeps the sequential run)")
63
+
64
+ def handle_arg_parsed(self, event: ArgParsedEvent) -> None:
65
+ parallel = getattr(event.args, 'uc_parallel', None)
66
+ if parallel:
67
+ self._parallel = max(int(parallel), 1)
68
+ self._refresh_worker_state()
69
+
70
+ async def handle_prepare_scenarios(self, event: StartupEvent) -> None:
71
+ self._refresh_worker_state()
72
+
73
+ if self._is_worker:
74
+ await ignore_foreign_scenarios(
75
+ event.scheduler,
76
+ self._worker_groups,
77
+ self._grouping_key,
78
+ replica_index=self._worker_replica_index,
79
+ replicas_total=self._env_replicas_total,
80
+ )
81
+ return
82
+
83
+ plan = await self._build_plan(event)
84
+ if plan is None or plan.workers_total <= 1:
85
+ return
86
+
87
+ set_execution_plan(plan)
88
+ self._register_runner()
89
+
90
+ async def _build_plan(self, event: StartupEvent):
91
+ env_parallelism = get_env_parallelism()
92
+ if env_parallelism is not None:
93
+ return await self._build_replica_plan(event, env_parallelism)
94
+ if self._parallel <= 1:
95
+ return None
96
+ return await build_execution_plan_from_scheduler(
97
+ event.scheduler,
98
+ self._parallel,
99
+ self._grouping_key,
100
+ )
101
+
102
+ async def _build_replica_plan(self, event: StartupEvent, env_parallelism):
103
+ counts_by_description, default_replicas = env_parallelism
104
+ group_counts = await collect_group_counts(event.scheduler, self._grouping_key)
105
+ group_envs = {}
106
+ namespace_counts = {}
107
+
108
+ async for scenario in event.scheduler:
109
+ if scenario.is_skipped():
110
+ continue
111
+ env = scenario_env(scenario)
112
+ group_key = env_group_key_for(env)
113
+ template = None if env is None or env.description == DEFAULT_ENV_DESCRIPTION else env
114
+ group_envs[group_key] = template
115
+ description = template.description if template is not None else DEFAULT_ENV_DESCRIPTION
116
+ namespace_counts[group_key] = counts_by_description.get(description, default_replicas)
117
+
118
+ return build_replica_execution_plan(
119
+ group_counts=group_counts,
120
+ group_envs=group_envs,
121
+ namespace_counts=namespace_counts,
122
+ default_replicas=default_replicas,
123
+ )
124
+
125
+ def _register_runner(self) -> None:
126
+ if self._runner_registered or self._is_worker or self._global_config is None:
127
+ return
128
+ dispatcher = self._global_config.Registry.Dispatcher()
129
+ self._global_config.Registry.ScenarioRunner.register(
130
+ resolver=lambda: ParallelScenarioRunner(
131
+ dispatcher=dispatcher,
132
+ grouping_key=self._grouping_key,
133
+ ),
134
+ registrant=self,
135
+ )
136
+ self._runner_registered = True
137
+
138
+
139
+ class VedroUberComposeParallel(PluginConfig):
140
+ plugin = VedroUberComposeParallelPlugin
141
+
142
+ enabled = False
143
+
144
+ parallel: int = 1
145
+
146
+ grouping_key = env_group_key
147
+
148
+
149
+ __all__ = ("VedroUberComposeParallel", "VedroUberComposeParallelPlugin",)