horsies 0.1.0a4__tar.gz → 0.1.0a5__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 (48) hide show
  1. {horsies-0.1.0a4 → horsies-0.1.0a5}/PKG-INFO +1 -1
  2. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/app.py +67 -47
  3. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/banner.py +27 -27
  4. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/brokers/postgres.py +315 -288
  5. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/cli.py +7 -2
  6. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/errors.py +3 -0
  7. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/models/app.py +87 -64
  8. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/models/recovery.py +30 -21
  9. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/models/schedule.py +30 -19
  10. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/models/tasks.py +1 -0
  11. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/models/workflow.py +489 -202
  12. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/models/workflow_pg.py +3 -1
  13. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/scheduler/service.py +5 -1
  14. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/scheduler/state.py +39 -27
  15. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/task_decorator.py +138 -0
  16. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/types/status.py +7 -5
  17. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/utils/imports.py +10 -10
  18. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/worker/worker.py +197 -139
  19. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/workflows/engine.py +487 -352
  20. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/workflows/recovery.py +148 -119
  21. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies.egg-info/PKG-INFO +1 -1
  22. {horsies-0.1.0a4 → horsies-0.1.0a5}/pyproject.toml +1 -1
  23. {horsies-0.1.0a4 → horsies-0.1.0a5}/README.md +0 -0
  24. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/__init__.py +0 -0
  25. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/__init__.py +0 -0
  26. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/brokers/__init__.py +0 -0
  27. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/brokers/listener.py +0 -0
  28. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/codec/serde.py +0 -0
  29. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/logging.py +0 -0
  30. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/models/__init__.py +0 -0
  31. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/models/broker.py +0 -0
  32. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/models/queues.py +0 -0
  33. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/models/task_pg.py +0 -0
  34. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/registry/tasks.py +0 -0
  35. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/scheduler/__init__.py +0 -0
  36. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/scheduler/calculator.py +0 -0
  37. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/utils/loop_runner.py +0 -0
  38. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/worker/current.py +0 -0
  39. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/workflows/__init__.py +0 -0
  40. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/core/workflows/registry.py +0 -0
  41. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies/py.typed +0 -0
  42. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies.egg-info/SOURCES.txt +0 -0
  43. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies.egg-info/dependency_links.txt +0 -0
  44. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies.egg-info/entry_points.txt +0 -0
  45. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies.egg-info/requires.txt +0 -0
  46. {horsies-0.1.0a4 → horsies-0.1.0a5}/horsies.egg-info/top_level.txt +0 -0
  47. {horsies-0.1.0a4 → horsies-0.1.0a5}/setup.cfg +0 -0
  48. {horsies-0.1.0a4 → horsies-0.1.0a5}/tests/test_issue_fixes.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: horsies
3
- Version: 0.1.0a4
3
+ Version: 0.1.0a5
4
4
  Summary: A Python library for distributed task execution
5
5
  Author: Suleyman Ozkeskin
6
6
  License-Expression: MIT
@@ -82,7 +82,9 @@ class Horsies:
82
82
  f'horsies subprocess initialized with {config.queue_mode.name} mode (pid={os.getpid()})'
83
83
  )
84
84
  else:
85
- self.logger.info(f'horsies initialized as {self._role} with {config.queue_mode.name} mode')
85
+ self.logger.info(
86
+ f'horsies initialized as {self._role} with {config.queue_mode.name} mode'
87
+ )
86
88
 
87
89
  def set_role(self, role: str) -> None:
88
90
  """Set the role and log it. Called by CLI after discovery."""
@@ -192,7 +194,7 @@ class Horsies:
192
194
  # Register task with this app, passing source for duplicate detection
193
195
  # Normalize path with realpath to handle symlinks and relative paths
194
196
  source_str = (
195
- f"{os.path.realpath(fn_location.file)}:{fn_location.line}"
197
+ f'{os.path.realpath(fn_location.file)}:{fn_location.line}'
196
198
  if fn_location
197
199
  else None
198
200
  )
@@ -247,15 +249,19 @@ class Horsies:
247
249
  if module_path.endswith('.py') or os.path.sep in module_path:
248
250
  abs_path = os.path.realpath(module_path)
249
251
  if not os.path.exists(abs_path):
250
- errors.append(_no_location(ConfigurationError(
251
- message=f'task module not found: {module_path}',
252
- code=ErrorCode.CLI_INVALID_ARGS,
253
- notes=[f'resolved path: {abs_path}'],
254
- help_text=(
255
- 'remove it from app.discover_tasks([...]) or fix the path; \n'
256
- 'if using globs, run app.expand_module_globs([...]) first'
257
- ),
258
- )))
252
+ errors.append(
253
+ _no_location(
254
+ ConfigurationError(
255
+ message=f'task module not found: {module_path}',
256
+ code=ErrorCode.CLI_INVALID_ARGS,
257
+ notes=[f'resolved path: {abs_path}'],
258
+ help_text=(
259
+ 'remove it from app.discover_tasks([...]) or fix the path; \n'
260
+ 'if using globs, run app.expand_module_globs([...]) first'
261
+ ),
262
+ )
263
+ )
264
+ )
259
265
  continue
260
266
  import_by_path(abs_path)
261
267
  else:
@@ -265,16 +271,20 @@ class Horsies:
265
271
  except HorsiesError as exc:
266
272
  errors.append(exc)
267
273
  except Exception as exc:
268
- errors.append(_no_location(ConfigurationError(
269
- message=f'failed to import module: {module_path}',
270
- code=ErrorCode.CLI_INVALID_ARGS,
271
- notes=[str(exc)],
272
- help_text=(
273
- 'ensure the module is importable; '
274
- 'for file paths include .py and a valid path, '
275
- 'for dotted paths verify PYTHONPATH or run from the project root'
276
- ),
277
- )))
274
+ errors.append(
275
+ _no_location(
276
+ ConfigurationError(
277
+ message=f'failed to import module: {module_path}',
278
+ code=ErrorCode.CLI_INVALID_ARGS,
279
+ notes=[str(exc)],
280
+ help_text=(
281
+ 'ensure the module is importable; '
282
+ 'for file paths include .py and a valid path, '
283
+ 'for dotted paths verify PYTHONPATH or run from the project root'
284
+ ),
285
+ )
286
+ )
287
+ )
278
288
  finally:
279
289
  self.suppress_sends(prev_suppress)
280
290
  return errors
@@ -283,26 +293,32 @@ class Horsies:
283
293
  """Check broker connectivity via SELECT 1."""
284
294
  import asyncio
285
295
 
296
+ from sqlalchemy import text
297
+
298
+ HEALTH_CHECK_SQL = text("""SELECT 1""")
299
+
286
300
  errors: list[HorsiesError] = []
287
301
  try:
288
302
  broker = self.get_broker()
289
303
 
290
304
  async def _test_connection() -> None:
291
- from sqlalchemy import text
292
-
293
305
  async with broker.session_factory() as session:
294
- await session.execute(text('SELECT 1'))
306
+ await session.execute(HEALTH_CHECK_SQL)
295
307
 
296
308
  asyncio.run(_test_connection())
297
309
  except HorsiesError as exc:
298
310
  errors.append(exc)
299
311
  except Exception as exc:
300
- errors.append(_no_location(ConfigurationError(
301
- message='broker connectivity check failed',
302
- code=ErrorCode.BROKER_INVALID_URL,
303
- notes=[str(exc)],
304
- help_text='check database_url in PostgresConfig',
305
- )))
312
+ errors.append(
313
+ _no_location(
314
+ ConfigurationError(
315
+ message='broker connectivity check failed',
316
+ code=ErrorCode.BROKER_INVALID_URL,
317
+ notes=[str(exc)],
318
+ help_text='check database_url in PostgresConfig',
319
+ )
320
+ )
321
+ )
306
322
  return errors
307
323
 
308
324
  def list_tasks(self) -> list[str]:
@@ -508,26 +524,30 @@ class Horsies:
508
524
  if self.config.queue_mode == QueueMode.CUSTOM:
509
525
  valid_queues = self.get_valid_queue_names()
510
526
  if resolved_queue not in valid_queues:
511
- report.add(ConfigurationError(
512
- message='TaskNode queue not in app config',
513
- code=ErrorCode.TASK_INVALID_QUEUE,
527
+ report.add(
528
+ ConfigurationError(
529
+ message='TaskNode queue not in app config',
530
+ code=ErrorCode.TASK_INVALID_QUEUE,
531
+ notes=[
532
+ f"TaskNode '{task.name}' has queue '{resolved_queue}'",
533
+ f'valid queues: {valid_queues}',
534
+ ],
535
+ help_text='use one of the configured queue names or add this queue to app config',
536
+ )
537
+ )
538
+ queue_valid = False
539
+ elif resolved_queue != 'default':
540
+ report.add(
541
+ ConfigurationError(
542
+ message='TaskNode has non-default queue in DEFAULT mode',
543
+ code=ErrorCode.CONFIG_INVALID_QUEUE_MODE,
514
544
  notes=[
515
545
  f"TaskNode '{task.name}' has queue '{resolved_queue}'",
516
- f'valid queues: {valid_queues}',
546
+ "app is in DEFAULT mode (only 'default' queue allowed)",
517
547
  ],
518
- help_text='use one of the configured queue names or add this queue to app config',
519
- ))
520
- queue_valid = False
521
- elif resolved_queue != 'default':
522
- report.add(ConfigurationError(
523
- message='TaskNode has non-default queue in DEFAULT mode',
524
- code=ErrorCode.CONFIG_INVALID_QUEUE_MODE,
525
- notes=[
526
- f"TaskNode '{task.name}' has queue '{resolved_queue}'",
527
- "app is in DEFAULT mode (only 'default' queue allowed)",
528
- ],
529
- help_text='either remove queue override or switch to QueueMode.CUSTOM',
530
- ))
548
+ help_text='either remove queue override or switch to QueueMode.CUSTOM',
549
+ )
550
+ )
531
551
  queue_valid = False
532
552
 
533
553
  if queue_valid:
@@ -55,22 +55,22 @@ def get_version() -> str:
55
55
  try:
56
56
  import horsies
57
57
 
58
- version = getattr(horsies, "__version__", None)
59
- return str(version) if version else "0.1.0"
58
+ version = getattr(horsies, '__version__', None)
59
+ return str(version) if version else '0.1.0'
60
60
  except ImportError:
61
- return "0.1.0"
61
+ return '0.1.0'
62
62
 
63
63
 
64
64
  def format_banner(version: str | None = None) -> str:
65
65
  """Format the banner string with version."""
66
66
  if version is None:
67
67
  version = get_version()
68
- return BANNER.format(version=f"v{version}")
68
+ return BANNER.format(version=f'v{version}')
69
69
 
70
70
 
71
71
  def print_banner(
72
- app: "Horsies",
73
- role: str = "worker",
72
+ app: 'Horsies',
73
+ role: str = 'worker',
74
74
  show_tasks: bool = True,
75
75
  file: TextIO | None = None,
76
76
  ) -> None:
@@ -96,44 +96,44 @@ def print_banner(
96
96
  lines.append(banner)
97
97
 
98
98
  # Configuration section
99
- lines.append("[config]")
100
- lines.append(f" .> app: {app.__class__.__name__}")
101
- lines.append(f" .> role: {role}")
102
- lines.append(f" .> queue_mode: {app.config.queue_mode.name}")
99
+ lines.append('[config]')
100
+ lines.append(f' .> app: {app.__class__.__name__}')
101
+ lines.append(f' .> role: {role}')
102
+ lines.append(f' .> queue_mode: {app.config.queue_mode.name}')
103
103
 
104
104
  # Queue info
105
- if app.config.queue_mode.name == "CUSTOM" and app.config.custom_queues:
106
- queues_str = ", ".join(q.name for q in app.config.custom_queues)
107
- lines.append(f" .> queues: {queues_str}")
105
+ if app.config.queue_mode.name == 'CUSTOM' and app.config.custom_queues:
106
+ queues_str = ', '.join(q.name for q in app.config.custom_queues)
107
+ lines.append(f' .> queues: {queues_str}')
108
108
  else:
109
- lines.append(f" .> queues: default")
109
+ lines.append(f' .> queues: default')
110
110
 
111
111
  # Broker info
112
112
  broker_url = app.config.broker.database_url
113
113
  # Mask password in URL
114
- if "@" in broker_url:
115
- pre, post = broker_url.split("@", 1)
116
- if ":" in pre:
117
- scheme_user = pre.rsplit(":", 1)[0]
118
- broker_url = f"{scheme_user}:****@{post}"
119
- lines.append(f" .> broker: {broker_url}")
114
+ if '@' in broker_url:
115
+ pre, post = broker_url.split('@', 1)
116
+ if ':' in pre:
117
+ scheme_user = pre.rsplit(':', 1)[0]
118
+ broker_url = f'{scheme_user}:****@{post}'
119
+ lines.append(f' .> broker: {broker_url}')
120
120
 
121
121
  # Concurrency info (if available)
122
- if hasattr(app.config, "cluster_wide_cap") and app.config.cluster_wide_cap:
123
- lines.append(f" .> cap: {app.config.cluster_wide_cap} (cluster-wide)")
122
+ if hasattr(app.config, 'cluster_wide_cap') and app.config.cluster_wide_cap:
123
+ lines.append(f' .> cap: {app.config.cluster_wide_cap} (cluster-wide)')
124
124
 
125
- lines.append("")
125
+ lines.append('')
126
126
 
127
127
  # Tasks section
128
128
  if show_tasks:
129
129
  task_names = app.list_tasks()
130
- lines.append(f"[tasks] ({len(task_names)} registered)")
130
+ lines.append(f'[tasks] ({len(task_names)} registered)')
131
131
  for task_name in sorted(task_names):
132
- lines.append(f" . {task_name}")
133
- lines.append("")
132
+ lines.append(f' . {task_name}')
133
+ lines.append('')
134
134
 
135
135
  # Print everything
136
- output = "\n".join(lines)
136
+ output = '\n'.join(lines)
137
137
  print(output, file=file)
138
138
 
139
139