horsies 0.1.0a4__py3-none-any.whl

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 (42) hide show
  1. horsies/__init__.py +117 -0
  2. horsies/core/__init__.py +0 -0
  3. horsies/core/app.py +552 -0
  4. horsies/core/banner.py +144 -0
  5. horsies/core/brokers/__init__.py +5 -0
  6. horsies/core/brokers/listener.py +444 -0
  7. horsies/core/brokers/postgres.py +993 -0
  8. horsies/core/cli.py +624 -0
  9. horsies/core/codec/serde.py +596 -0
  10. horsies/core/errors.py +535 -0
  11. horsies/core/logging.py +90 -0
  12. horsies/core/models/__init__.py +0 -0
  13. horsies/core/models/app.py +268 -0
  14. horsies/core/models/broker.py +79 -0
  15. horsies/core/models/queues.py +23 -0
  16. horsies/core/models/recovery.py +101 -0
  17. horsies/core/models/schedule.py +229 -0
  18. horsies/core/models/task_pg.py +307 -0
  19. horsies/core/models/tasks.py +358 -0
  20. horsies/core/models/workflow.py +1990 -0
  21. horsies/core/models/workflow_pg.py +245 -0
  22. horsies/core/registry/tasks.py +101 -0
  23. horsies/core/scheduler/__init__.py +26 -0
  24. horsies/core/scheduler/calculator.py +267 -0
  25. horsies/core/scheduler/service.py +569 -0
  26. horsies/core/scheduler/state.py +260 -0
  27. horsies/core/task_decorator.py +656 -0
  28. horsies/core/types/status.py +38 -0
  29. horsies/core/utils/imports.py +203 -0
  30. horsies/core/utils/loop_runner.py +44 -0
  31. horsies/core/worker/current.py +17 -0
  32. horsies/core/worker/worker.py +1967 -0
  33. horsies/core/workflows/__init__.py +23 -0
  34. horsies/core/workflows/engine.py +2344 -0
  35. horsies/core/workflows/recovery.py +501 -0
  36. horsies/core/workflows/registry.py +97 -0
  37. horsies/py.typed +0 -0
  38. horsies-0.1.0a4.dist-info/METADATA +35 -0
  39. horsies-0.1.0a4.dist-info/RECORD +42 -0
  40. horsies-0.1.0a4.dist-info/WHEEL +5 -0
  41. horsies-0.1.0a4.dist-info/entry_points.txt +2 -0
  42. horsies-0.1.0a4.dist-info/top_level.txt +1 -0
horsies/core/cli.py ADDED
@@ -0,0 +1,624 @@
1
+ # app/core/cli.py
2
+ """
3
+ CLI for horsies worker, scheduler, and check commands.
4
+
5
+ Module path resolution follows Celery's approach:
6
+ 1. User provides dotted module path: `horsies worker app.configs.horsies:app`
7
+ 2. User is responsible for PYTHONPATH / running from correct directory
8
+ 3. Convenience: if cwd has pyproject.toml, we add cwd to sys.path
9
+ """
10
+
11
+ import argparse
12
+ import asyncio
13
+ import importlib
14
+ import logging
15
+ import os
16
+ import signal
17
+ import sys
18
+
19
+ from horsies.core.app import Horsies
20
+ from horsies.core.banner import print_banner
21
+ from horsies.core.errors import ConfigurationError, ErrorCode, HorsiesError, ValidationReport
22
+ from horsies.core.logging import get_logger
23
+ from horsies.core.scheduler import Scheduler
24
+ from horsies.core.worker.worker import Worker, WorkerConfig
25
+ from horsies.core.utils.imports import (
26
+ import_file_path,
27
+ setup_sys_path_from_cwd,
28
+ )
29
+
30
+
31
+ def _resolve_module_argument(args: argparse.Namespace) -> str:
32
+ """Return module path from --module or positional, error if missing."""
33
+ module_path = getattr(args, 'module', None) or getattr(args, 'module_pos', None)
34
+ if not module_path:
35
+ raise ConfigurationError(
36
+ message='module path is required',
37
+ code=ErrorCode.CLI_INVALID_ARGS,
38
+ notes=['no --module flag or positional module argument provided'],
39
+ help_text=(
40
+ 'provide module path in one of these formats:\n'
41
+ ' horsies worker app.configs.horsies:app (recommended)\n'
42
+ ' horsies worker app/configs/horsies.py:app (file path)\n'
43
+ ' horsies worker app.configs.horsies (auto-discover app variable)'
44
+ ),
45
+ )
46
+ return module_path
47
+
48
+
49
+ def _parse_locator(locator: str) -> tuple[str, str | None]:
50
+ """
51
+ Parse a module locator into (module_path, attribute_name).
52
+
53
+ Formats:
54
+ - "app.configs.horsies:app" -> ("app.configs.horsies", "app")
55
+ - "app.configs.horsies" -> ("app.configs.horsies", None)
56
+ - "/path/to/file.py:app" -> ("/path/to/file.py", "app")
57
+ - "app/configs/horsies.py" -> ("app/configs/horsies.py", None)
58
+ """
59
+ if ':' in locator:
60
+ module_part, attr = locator.rsplit(':', 1)
61
+ return (module_part, attr)
62
+ return (locator, None)
63
+
64
+
65
+ def _is_file_path(path: str) -> bool:
66
+ """Check if path looks like a file path (vs dotted module path)."""
67
+ return path.endswith('.py') or os.path.sep in path or '/' in path
68
+
69
+
70
+ def discover_app(module_locator: str) -> tuple[Horsies, str, str, str | None]:
71
+ """
72
+ Import module and discover horsies instance.
73
+
74
+ Supports two formats (like Celery):
75
+ 1. Dotted module path: "app.configs.horsies:app" or "app.configs.horsies"
76
+ 2. File path: "app/configs/horsies.py:app" or "app/configs/horsies.py"
77
+
78
+ The pyproject.toml convenience is applied before import:
79
+ if cwd contains pyproject.toml, cwd is added to sys.path.
80
+
81
+ Returns:
82
+ (app_instance, variable_name, module_name, sys_path_root)
83
+ """
84
+ logger = get_logger('cli')
85
+
86
+ # Convenience: add project root to sys.path if pyproject.toml found
87
+ project_root = setup_sys_path_from_cwd()
88
+ if project_root:
89
+ logger.info(f'Added project root to sys.path: {project_root}')
90
+
91
+ # Parse the locator
92
+ module_path, attr_name = _parse_locator(module_locator)
93
+
94
+ # Import the module
95
+ if _is_file_path(module_path):
96
+ # File path - normalize and import
97
+ if not module_path.endswith('.py'):
98
+ module_path += '.py'
99
+ file_path = os.path.realpath(module_path)
100
+
101
+ if not os.path.exists(file_path):
102
+ # Detect ambiguous "dotted.module.py" pattern
103
+ stem = module_path.removesuffix('.py')
104
+ if '.' in stem and '/' not in stem and os.path.sep not in stem:
105
+ attr_hint = attr_name if attr_name else '<app_name>'
106
+ dotted_form = f'{stem}:{attr_hint}'
107
+ file_form = f'{stem.replace(".", "/")}.py:{attr_hint}'
108
+ help_lines = (
109
+ f'use dotted module path: {dotted_form}\n'
110
+ f'or use file path: {file_form}'
111
+ )
112
+ if not attr_name:
113
+ help_lines += '\nwhere <app_name> is the variable name of your Horsies instance'
114
+ raise ConfigurationError(
115
+ message=f"ambiguous module locator: '{module_locator}'",
116
+ code=ErrorCode.CLI_INVALID_ARGS,
117
+ notes=[
118
+ f"'{module_path}' mixes dotted module notation with a .py file extension",
119
+ ],
120
+ help_text=help_lines,
121
+ )
122
+ raise FileNotFoundError(f'Module file not found: {file_path}')
123
+
124
+ # import_file_path adds parent directory to sys.path
125
+ module = import_file_path(file_path)
126
+ module_name = module.__name__
127
+ sys_path_root = os.path.dirname(file_path)
128
+ else:
129
+ # Dotted module path - use standard import
130
+ try:
131
+ module = importlib.import_module(module_path)
132
+ except ModuleNotFoundError as e:
133
+ raise ConfigurationError(
134
+ message=f'module not found: {module_path}',
135
+ code=ErrorCode.CLI_INVALID_ARGS,
136
+ notes=[
137
+ str(e),
138
+ f'sys.path: {sys.path[:5]}...',
139
+ ],
140
+ help_text=(
141
+ 'ensure you are running from the correct directory\n'
142
+ 'or set PYTHONPATH to include your project root'
143
+ ),
144
+ )
145
+ module_name = module_path
146
+ sys_path_root = project_root
147
+
148
+ # Find horsies instance
149
+ if attr_name:
150
+ # Explicit attribute name
151
+ if not hasattr(module, attr_name):
152
+ raise AttributeError(
153
+ f"Module '{module_name}' has no attribute '{attr_name}'"
154
+ )
155
+ obj = getattr(module, attr_name)
156
+ if not isinstance(obj, Horsies):
157
+ raise TypeError(
158
+ f"'{attr_name}' in module '{module_name}' is not a Horsies instance "
159
+ f"(got {type(obj).__name__})"
160
+ )
161
+ app = obj
162
+ var_name = attr_name
163
+ else:
164
+ # Auto-discover horsies instance
165
+ app_instances: list[tuple[Horsies, str]] = []
166
+ for name in dir(module):
167
+ if not name.startswith('_'):
168
+ obj = getattr(module, name)
169
+ if isinstance(obj, Horsies):
170
+ app_instances.append((obj, name))
171
+
172
+ if not app_instances:
173
+ raise AttributeError(
174
+ f'No Horsies instance found in {module_name}. '
175
+ 'Specify the variable name: module.path:variable'
176
+ )
177
+
178
+ if len(app_instances) > 1:
179
+ var_names = [name for _, name in app_instances]
180
+ raise AttributeError(
181
+ f'Multiple Horsies instances found in {module_name}: {var_names}. '
182
+ 'Specify which one: module.path:variable'
183
+ )
184
+
185
+ app, var_name = app_instances[0]
186
+
187
+ logger.info(f"Discovered horsies '{var_name}' from {module_name}")
188
+ return app, var_name, module_name, sys_path_root
189
+
190
+
191
+ def setup_logging(loglevel: str) -> None:
192
+ """Configure logging level globally."""
193
+ from horsies.core.logging import set_default_level
194
+
195
+ level = getattr(logging, loglevel.upper(), logging.INFO)
196
+ set_default_level(level)
197
+
198
+ root_logger = logging.getLogger('horsies')
199
+ root_logger.setLevel(level)
200
+
201
+ for handler in root_logger.handlers:
202
+ handler.setLevel(level)
203
+
204
+ for name in logging.Logger.manager.loggerDict:
205
+ if isinstance(name, str) and name.startswith('horsies.'):
206
+ lgr = logging.getLogger(name)
207
+ lgr.setLevel(level)
208
+ for handler in lgr.handlers:
209
+ handler.setLevel(level)
210
+
211
+
212
+ def worker_command(args: argparse.Namespace) -> None:
213
+ """Handle worker command."""
214
+ logger = get_logger('cli')
215
+
216
+ # Setup logging first
217
+ loglevel: str = args.loglevel
218
+ setup_logging(loglevel)
219
+ logger.info(f'Starting horsies worker with loglevel={loglevel}')
220
+
221
+ # Discover app
222
+ try:
223
+ module_locator: str = _resolve_module_argument(args)
224
+ app, var_name, module_name, sys_path_root = discover_app(module_locator)
225
+ app.set_role('worker')
226
+ except HorsiesError as e:
227
+ logger.error(str(e))
228
+ sys.exit(1)
229
+ except ValueError as e:
230
+ logger.error(str(e))
231
+ sys.exit(1)
232
+ except Exception as e:
233
+ logger.error(f'Failed to discover app: {e}')
234
+ sys.exit(1)
235
+
236
+ # Get broker config from app
237
+ try:
238
+ broker = app.get_broker()
239
+ postgres_config = broker.config
240
+ except Exception as e:
241
+ logger.error(f'Failed to get broker config: {e}')
242
+ sys.exit(1)
243
+
244
+ # Get queues from app config
245
+ queues: list[str] = app.get_valid_queue_names()
246
+ logger.info(f'Worker will process queues: {queues}')
247
+
248
+ # Build per-queue settings for CUSTOM mode
249
+ queue_priorities: dict[str, int] = {}
250
+ queue_max_concurrency: dict[str, int] = {}
251
+ try:
252
+ if app.config.queue_mode.name == 'CUSTOM' and app.config.custom_queues:
253
+ for q in app.config.custom_queues:
254
+ queue_priorities[q.name] = q.priority
255
+ queue_max_concurrency[q.name] = q.max_concurrency
256
+ except Exception:
257
+ pass
258
+
259
+ # Get discovered task modules
260
+ discovered_modules = app.get_discovered_task_modules()
261
+ if discovered_modules:
262
+ logger.info(f'Using discovered task modules')
263
+ else:
264
+ logger.warning('No task modules discovered.')
265
+
266
+ # Create worker config
267
+ processes: int = args.processes
268
+ log_level_int = getattr(logging, loglevel.upper(), logging.INFO)
269
+
270
+ # Build sys_path_roots
271
+ sys_path_roots: list[str] = []
272
+ if sys_path_root:
273
+ sys_path_roots.append(sys_path_root)
274
+
275
+ # Build app_locator - prefer module path format
276
+ if _is_file_path(module_locator.split(':')[0]):
277
+ # File path - use absolute path
278
+ file_path = module_locator.split(':')[0]
279
+ if not file_path.endswith('.py'):
280
+ file_path += '.py'
281
+ app_locator = f'{os.path.realpath(file_path)}:{var_name}'
282
+ else:
283
+ # Module path - use as-is
284
+ app_locator = f'{module_name}:{var_name}'
285
+
286
+ worker_config = WorkerConfig(
287
+ dsn=postgres_config.database_url,
288
+ psycopg_dsn=postgres_config.database_url,
289
+ queues=queues,
290
+ processes=processes,
291
+ app_locator=app_locator,
292
+ sys_path_roots=sys_path_roots,
293
+ imports=discovered_modules,
294
+ queue_priorities=queue_priorities,
295
+ queue_max_concurrency=queue_max_concurrency,
296
+ cluster_wide_cap=app.config.cluster_wide_cap,
297
+ prefetch_buffer=app.config.prefetch_buffer,
298
+ claim_lease_ms=app.config.claim_lease_ms,
299
+ max_claim_batch=args.max_claim_batch,
300
+ max_claim_per_worker=args.max_claim_per_worker,
301
+ recovery_config=app.config.recovery,
302
+ loglevel=log_level_int,
303
+ )
304
+
305
+ # Print startup banner
306
+ app.import_task_modules() # Import tasks so they show in banner
307
+ print_banner(app, role='worker', show_tasks=True)
308
+
309
+ # Start worker
310
+ logger.info('Starting worker...')
311
+ try:
312
+
313
+ async def run_worker() -> None:
314
+ try:
315
+ logger.info('Ensuring Postgres schema and triggers are initialized...')
316
+ await broker.ensure_schema_initialized()
317
+ except Exception as e:
318
+ logger.error(f'Failed to initialize database schema: {e}')
319
+ raise
320
+
321
+ worker = Worker(broker.session_factory, broker.listener, worker_config)
322
+
323
+ loop = asyncio.get_running_loop()
324
+
325
+ def signal_handler() -> None:
326
+ logger.info('Received interrupt signal, stopping worker...')
327
+ worker.request_stop()
328
+
329
+ for sig in (signal.SIGTERM, signal.SIGINT):
330
+ try:
331
+ loop.add_signal_handler(sig, signal_handler)
332
+ except NotImplementedError:
333
+ pass
334
+
335
+ await worker.run_forever()
336
+
337
+ try:
338
+ asyncio.run(run_worker())
339
+ except KeyboardInterrupt:
340
+ logger.info('Worker interrupted by user')
341
+ return
342
+ except asyncio.TimeoutError:
343
+ logger.error('Worker startup timed out')
344
+ return
345
+ except KeyboardInterrupt:
346
+ logger.info('Worker interrupted by user')
347
+ return
348
+ except Exception as e:
349
+ logger.error(f'Worker failed: {e}')
350
+ sys.exit(1)
351
+
352
+
353
+ def scheduler_command(args: argparse.Namespace) -> None:
354
+ """Handle scheduler command."""
355
+ logger = get_logger('cli')
356
+
357
+ # Setup logging first
358
+ loglevel: str = args.loglevel
359
+ setup_logging(loglevel)
360
+ logger.info(f'Starting scheduler with loglevel={loglevel}')
361
+
362
+ # Discover app
363
+ try:
364
+ module_locator: str = _resolve_module_argument(args)
365
+ app, _var_name, _module_name, _sys_path_root = discover_app(module_locator)
366
+ app.set_role('scheduler')
367
+ except HorsiesError as e:
368
+ logger.error(str(e))
369
+ sys.exit(1)
370
+ except ValueError as e:
371
+ logger.error(str(e))
372
+ sys.exit(1)
373
+ except Exception as e:
374
+ logger.error(f'Failed to discover app: {e}')
375
+ sys.exit(1)
376
+
377
+ # Validate schedule config
378
+ if not app.config.schedule:
379
+ logger.error('Schedule configuration not found in app config')
380
+ sys.exit(1)
381
+
382
+ if not app.config.schedule.enabled:
383
+ logger.warning('Scheduler is disabled in config')
384
+ sys.exit(1)
385
+
386
+ # Import task modules for validation
387
+ discovered_modules = app.get_discovered_task_modules()
388
+ if discovered_modules:
389
+ logger.info(f'Task modules available: {discovered_modules}')
390
+ for module in discovered_modules:
391
+ try:
392
+ if _is_file_path(module):
393
+ import_file_path(os.path.abspath(module))
394
+ else:
395
+ importlib.import_module(module)
396
+ except Exception as e:
397
+ logger.warning(f"Failed to import task module '{module}': {e}")
398
+
399
+ schedule_count = len(app.config.schedule.schedules)
400
+ enabled_count = sum(1 for s in app.config.schedule.schedules if s.enabled)
401
+
402
+ # Print startup banner
403
+ print_banner(app, role='scheduler', show_tasks=True)
404
+
405
+ # Start scheduler
406
+ logger.info(f'Scheduler: {enabled_count}/{schedule_count} schedules enabled')
407
+ try:
408
+
409
+ async def run_scheduler() -> None:
410
+ try:
411
+ scheduler = Scheduler(app)
412
+
413
+ loop = asyncio.get_running_loop()
414
+
415
+ def signal_handler() -> None:
416
+ logger.info('Received interrupt signal, stopping scheduler...')
417
+ scheduler.request_stop()
418
+
419
+ for sig in (signal.SIGTERM, signal.SIGINT):
420
+ try:
421
+ loop.add_signal_handler(sig, signal_handler)
422
+ except NotImplementedError:
423
+ pass
424
+
425
+ await scheduler.run_forever()
426
+
427
+ except Exception as e:
428
+ logger.error(f'Scheduler error: {e}', exc_info=True)
429
+ raise
430
+
431
+ try:
432
+ asyncio.run(run_scheduler())
433
+ except KeyboardInterrupt:
434
+ logger.info('Scheduler interrupted by user')
435
+ return
436
+
437
+ except KeyboardInterrupt:
438
+ logger.info('Scheduler interrupted by user')
439
+ return
440
+ except Exception as e:
441
+ logger.error(f'Scheduler failed: {e}')
442
+ sys.exit(1)
443
+
444
+
445
+ def check_command(args: argparse.Namespace) -> None:
446
+ """Handle check command — validate app configuration without starting services."""
447
+ logger = get_logger('cli')
448
+
449
+ # Setup logging
450
+ loglevel: str = args.loglevel
451
+ setup_logging(loglevel)
452
+
453
+ # Discover app
454
+ try:
455
+ module_locator: str = _resolve_module_argument(args)
456
+ app, _var_name, _module_name, _sys_path_root = discover_app(module_locator)
457
+ except HorsiesError as e:
458
+ logger.error(str(e))
459
+ sys.exit(1)
460
+ except ValueError as e:
461
+ logger.error(str(e))
462
+ sys.exit(1)
463
+ except Exception as e:
464
+ logger.error(f'Failed to discover app: {e}')
465
+ sys.exit(1)
466
+
467
+ # Run phased validation
468
+ live: bool = args.live
469
+ errors = app.check(live=live)
470
+
471
+ if errors:
472
+ report = ValidationReport('check')
473
+ for error in errors:
474
+ report.add(error)
475
+ print(report.format_rust_style(), file=sys.stderr)
476
+ sys.exit(1)
477
+ else:
478
+ task_count = len(app.list_tasks())
479
+ print(f'ok: all validations passed\n {task_count} task(s) registered')
480
+ sys.exit(0)
481
+
482
+
483
+ def main() -> None:
484
+ """Main CLI entry point."""
485
+ try:
486
+ parser = argparse.ArgumentParser(
487
+ prog='horsies',
488
+ description='Horsies task queue - worker and scheduler management',
489
+ formatter_class=argparse.RawDescriptionHelpFormatter,
490
+ epilog="""
491
+ Examples:
492
+ # Using dotted module path (recommended)
493
+ horsies worker app.configs.horsies:app
494
+
495
+ # Using file path
496
+ horsies worker app/configs/horsies.py:app
497
+
498
+ # Auto-discover app variable
499
+ horsies worker app.configs.horsies
500
+
501
+ # Validate configuration without starting services
502
+ horsies check app.configs.horsies:app
503
+ horsies check app.configs.horsies:app --live
504
+ """,
505
+ )
506
+ subparsers = parser.add_subparsers(dest='command', help='Available commands')
507
+
508
+ # Worker command
509
+ worker_parser = subparsers.add_parser(
510
+ 'worker',
511
+ help='Start a horsies worker',
512
+ formatter_class=argparse.RawDescriptionHelpFormatter,
513
+ )
514
+ worker_parser.add_argument(
515
+ '-m',
516
+ '--module',
517
+ dest='module',
518
+ help='Module path (e.g., app.configs.horsies:app)',
519
+ )
520
+ worker_parser.add_argument(
521
+ 'module_pos',
522
+ nargs='?',
523
+ help='Module path (e.g., app.configs.horsies:app)',
524
+ )
525
+ worker_parser.add_argument(
526
+ '--loglevel',
527
+ choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
528
+ default='INFO',
529
+ type=str.upper,
530
+ help='Logging level (default: INFO)',
531
+ )
532
+ worker_parser.add_argument(
533
+ '--processes',
534
+ type=int,
535
+ default=1,
536
+ help='Number of worker processes (default: 1)',
537
+ )
538
+ worker_parser.add_argument(
539
+ '--max-claim-batch',
540
+ type=int,
541
+ default=2,
542
+ help='Max tasks per queue per pass (default: 2)',
543
+ )
544
+ worker_parser.add_argument(
545
+ '--max-claim-per-worker',
546
+ type=int,
547
+ default=0,
548
+ help='Max claimed tasks per worker, 0=auto (default: 0)',
549
+ )
550
+
551
+ # Scheduler command
552
+ scheduler_parser = subparsers.add_parser(
553
+ 'scheduler',
554
+ help='Start the scheduler service',
555
+ )
556
+ scheduler_parser.add_argument(
557
+ '-m',
558
+ '--module',
559
+ dest='module',
560
+ help='Module path (e.g., app.configs.horsies:app)',
561
+ )
562
+ scheduler_parser.add_argument(
563
+ 'module_pos',
564
+ nargs='?',
565
+ help='Module path (e.g., app.configs.horsies:app)',
566
+ )
567
+ scheduler_parser.add_argument(
568
+ '--loglevel',
569
+ choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
570
+ default='INFO',
571
+ type=str.upper,
572
+ help='Logging level (default: INFO)',
573
+ )
574
+
575
+ # Check command
576
+ check_parser = subparsers.add_parser(
577
+ 'check',
578
+ help='Validate app configuration without starting services',
579
+ formatter_class=argparse.RawDescriptionHelpFormatter,
580
+ )
581
+ check_parser.add_argument(
582
+ '-m',
583
+ '--module',
584
+ dest='module',
585
+ help='Module path (e.g., app.configs.horsies:app)',
586
+ )
587
+ check_parser.add_argument(
588
+ 'module_pos',
589
+ nargs='?',
590
+ help='Module path (e.g., app.configs.horsies:app)',
591
+ )
592
+ check_parser.add_argument(
593
+ '--loglevel',
594
+ choices=['DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'],
595
+ default='WARNING',
596
+ type=str.upper,
597
+ help='Logging level (default: WARNING)',
598
+ )
599
+ check_parser.add_argument(
600
+ '--live',
601
+ action='store_true',
602
+ default=False,
603
+ help='Also check broker connectivity (SELECT 1)',
604
+ )
605
+
606
+ args = parser.parse_args()
607
+
608
+ match args.command:
609
+ case 'worker':
610
+ worker_command(args)
611
+ case 'scheduler':
612
+ scheduler_command(args)
613
+ case 'check':
614
+ check_command(args)
615
+ case _:
616
+ parser.print_help()
617
+ sys.exit(1)
618
+ except KeyboardInterrupt:
619
+ print('\nInterrupted by user')
620
+ sys.exit(0)
621
+
622
+
623
+ if __name__ == '__main__':
624
+ main()