horsies 0.1.0a4__py3-none-any.whl → 0.1.0a5__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.
horsies/core/cli.py CHANGED
@@ -18,7 +18,12 @@ import sys
18
18
 
19
19
  from horsies.core.app import Horsies
20
20
  from horsies.core.banner import print_banner
21
- from horsies.core.errors import ConfigurationError, ErrorCode, HorsiesError, ValidationReport
21
+ from horsies.core.errors import (
22
+ ConfigurationError,
23
+ ErrorCode,
24
+ HorsiesError,
25
+ ValidationReport,
26
+ )
22
27
  from horsies.core.logging import get_logger
23
28
  from horsies.core.scheduler import Scheduler
24
29
  from horsies.core.worker.worker import Worker, WorkerConfig
@@ -156,7 +161,7 @@ def discover_app(module_locator: str) -> tuple[Horsies, str, str, str | None]:
156
161
  if not isinstance(obj, Horsies):
157
162
  raise TypeError(
158
163
  f"'{attr_name}' in module '{module_name}' is not a Horsies instance "
159
- f"(got {type(obj).__name__})"
164
+ f'(got {type(obj).__name__})'
160
165
  )
161
166
  app = obj
162
167
  var_name = attr_name
horsies/core/errors.py CHANGED
@@ -38,6 +38,9 @@ class ErrorCode(str, Enum):
38
38
  WORKFLOW_INVALID_JOIN = 'E013'
39
39
  WORKFLOW_UNRESOLVED_QUEUE = 'E014'
40
40
  WORKFLOW_UNRESOLVED_PRIORITY = 'E015'
41
+ WORKFLOW_ARGS_WITH_INJECTION = 'E016'
42
+ WORKFLOW_INVALID_KWARG_KEY = 'E019'
43
+ WORKFLOW_MISSING_REQUIRED_PARAMS = 'E020'
41
44
  WORKFLOW_INVALID_SUBWORKFLOW_RETRY_MODE = 'E017'
42
45
  WORKFLOW_SUBWORKFLOW_APP_MISSING = 'E018'
43
46
 
@@ -5,7 +5,12 @@ from horsies.core.models.queues import QueueMode, CustomQueueConfig
5
5
  from horsies.core.models.broker import PostgresConfig
6
6
  from horsies.core.models.recovery import RecoveryConfig
7
7
  from horsies.core.models.schedule import ScheduleConfig
8
- from horsies.core.errors import ConfigurationError, ErrorCode, ValidationReport, raise_collected
8
+ from horsies.core.errors import (
9
+ ConfigurationError,
10
+ ErrorCode,
11
+ ValidationReport,
12
+ raise_collected,
13
+ )
9
14
  from urllib.parse import urlparse, urlunparse
10
15
  import logging
11
16
  import os
@@ -37,91 +42,109 @@ class AppConfig(BaseModel):
37
42
 
38
43
  if self.queue_mode == QueueMode.DEFAULT:
39
44
  if self.custom_queues is not None:
40
- report.add(ConfigurationError(
41
- message='custom_queues must be None in DEFAULT mode',
42
- code=ErrorCode.CONFIG_INVALID_QUEUE_MODE,
43
- notes=['queue_mode=DEFAULT but custom_queues was provided'],
44
- help_text='either remove custom_queues or set queue_mode=CUSTOM',
45
- ))
45
+ report.add(
46
+ ConfigurationError(
47
+ message='custom_queues must be None in DEFAULT mode',
48
+ code=ErrorCode.CONFIG_INVALID_QUEUE_MODE,
49
+ notes=['queue_mode=DEFAULT but custom_queues was provided'],
50
+ help_text='either remove custom_queues or set queue_mode=CUSTOM',
51
+ )
52
+ )
46
53
  elif self.queue_mode == QueueMode.CUSTOM:
47
54
  if self.custom_queues is None or len(self.custom_queues) == 0:
48
- report.add(ConfigurationError(
49
- message='custom_queues required in CUSTOM mode',
50
- code=ErrorCode.CONFIG_INVALID_QUEUE_MODE,
51
- notes=['queue_mode=CUSTOM but custom_queues is empty or None'],
52
- help_text='provide at least one CustomQueueConfig in custom_queues',
53
- ))
55
+ report.add(
56
+ ConfigurationError(
57
+ message='custom_queues required in CUSTOM mode',
58
+ code=ErrorCode.CONFIG_INVALID_QUEUE_MODE,
59
+ notes=['queue_mode=CUSTOM but custom_queues is empty or None'],
60
+ help_text='provide at least one CustomQueueConfig in custom_queues',
61
+ )
62
+ )
54
63
  else:
55
64
  # Validate unique queue names (only if queues exist)
56
65
  queue_names = [q.name for q in self.custom_queues]
57
66
  if len(queue_names) != len(set(queue_names)):
58
- report.add(ConfigurationError(
59
- message='duplicate queue names in custom_queues',
60
- code=ErrorCode.CONFIG_INVALID_QUEUE_MODE,
61
- notes=[f'queue names: {queue_names}'],
62
- help_text='each queue name must be unique',
63
- ))
67
+ report.add(
68
+ ConfigurationError(
69
+ message='duplicate queue names in custom_queues',
70
+ code=ErrorCode.CONFIG_INVALID_QUEUE_MODE,
71
+ notes=[f'queue names: {queue_names}'],
72
+ help_text='each queue name must be unique',
73
+ )
74
+ )
64
75
 
65
76
  # Validate cluster_wide_cap if provided
66
77
  if self.cluster_wide_cap is not None and self.cluster_wide_cap <= 0:
67
- report.add(ConfigurationError(
68
- message='cluster_wide_cap must be positive',
69
- code=ErrorCode.CONFIG_INVALID_CLUSTER_CAP,
70
- notes=[f'got cluster_wide_cap={self.cluster_wide_cap}'],
71
- help_text='use a positive integer or None for unlimited',
72
- ))
78
+ report.add(
79
+ ConfigurationError(
80
+ message='cluster_wide_cap must be positive',
81
+ code=ErrorCode.CONFIG_INVALID_CLUSTER_CAP,
82
+ notes=[f'got cluster_wide_cap={self.cluster_wide_cap}'],
83
+ help_text='use a positive integer or None for unlimited',
84
+ )
85
+ )
73
86
 
74
87
  # Validate prefetch_buffer
75
88
  if self.prefetch_buffer < 0:
76
- report.add(ConfigurationError(
77
- message='prefetch_buffer must be non-negative',
78
- code=ErrorCode.CONFIG_INVALID_PREFETCH,
79
- notes=[f'got prefetch_buffer={self.prefetch_buffer}'],
80
- help_text='use 0 for hard cap mode or positive integer for soft cap',
81
- ))
89
+ report.add(
90
+ ConfigurationError(
91
+ message='prefetch_buffer must be non-negative',
92
+ code=ErrorCode.CONFIG_INVALID_PREFETCH,
93
+ notes=[f'got prefetch_buffer={self.prefetch_buffer}'],
94
+ help_text='use 0 for hard cap mode or positive integer for soft cap',
95
+ )
96
+ )
82
97
 
83
98
  # Validate claim_lease_ms
84
99
  if self.prefetch_buffer > 0 and self.claim_lease_ms is None:
85
- report.add(ConfigurationError(
86
- message='claim_lease_ms required when prefetch_buffer > 0',
87
- code=ErrorCode.CONFIG_INVALID_PREFETCH,
88
- notes=[
89
- f'prefetch_buffer={self.prefetch_buffer} but claim_lease_ms is None',
90
- ],
91
- help_text='set claim_lease_ms (e.g., 30000 for 30 seconds)',
92
- ))
100
+ report.add(
101
+ ConfigurationError(
102
+ message='claim_lease_ms required when prefetch_buffer > 0',
103
+ code=ErrorCode.CONFIG_INVALID_PREFETCH,
104
+ notes=[
105
+ f'prefetch_buffer={self.prefetch_buffer} but claim_lease_ms is None',
106
+ ],
107
+ help_text='set claim_lease_ms (e.g., 30000 for 30 seconds)',
108
+ )
109
+ )
93
110
  if self.claim_lease_ms is not None and self.claim_lease_ms <= 0:
94
- report.add(ConfigurationError(
95
- message='claim_lease_ms must be positive',
96
- code=ErrorCode.CONFIG_INVALID_PREFETCH,
97
- notes=[f'got claim_lease_ms={self.claim_lease_ms}'],
98
- help_text='use a positive integer in milliseconds',
99
- ))
111
+ report.add(
112
+ ConfigurationError(
113
+ message='claim_lease_ms must be positive',
114
+ code=ErrorCode.CONFIG_INVALID_PREFETCH,
115
+ notes=[f'got claim_lease_ms={self.claim_lease_ms}'],
116
+ help_text='use a positive integer in milliseconds',
117
+ )
118
+ )
100
119
 
101
120
  # Forbid claim_lease_ms in hard cap mode
102
121
  if self.prefetch_buffer == 0 and self.claim_lease_ms is not None:
103
- report.add(ConfigurationError(
104
- message='claim_lease_ms incompatible with hard cap mode',
105
- code=ErrorCode.CONFIG_INVALID_PREFETCH,
106
- notes=[
107
- 'prefetch_buffer=0 (hard cap mode)',
108
- f'but claim_lease_ms={self.claim_lease_ms} was set',
109
- ],
110
- help_text='remove claim_lease_ms or set prefetch_buffer > 0',
111
- ))
122
+ report.add(
123
+ ConfigurationError(
124
+ message='claim_lease_ms incompatible with hard cap mode',
125
+ code=ErrorCode.CONFIG_INVALID_PREFETCH,
126
+ notes=[
127
+ 'prefetch_buffer=0 (hard cap mode)',
128
+ f'but claim_lease_ms={self.claim_lease_ms} was set',
129
+ ],
130
+ help_text='remove claim_lease_ms or set prefetch_buffer > 0',
131
+ )
132
+ )
112
133
 
113
134
  # Validate prefetch_buffer vs cluster_wide_cap conflict
114
135
  if self.cluster_wide_cap is not None and self.prefetch_buffer > 0:
115
- report.add(ConfigurationError(
116
- message='cluster_wide_cap incompatible with prefetch mode',
117
- code=ErrorCode.CONFIG_INVALID_CLUSTER_CAP,
118
- notes=[
119
- f'cluster_wide_cap={self.cluster_wide_cap}',
120
- f'prefetch_buffer={self.prefetch_buffer}',
121
- 'cluster_wide_cap requires hard cap mode (prefetch_buffer=0)',
122
- ],
123
- help_text='set prefetch_buffer=0 when using cluster_wide_cap',
124
- ))
136
+ report.add(
137
+ ConfigurationError(
138
+ message='cluster_wide_cap incompatible with prefetch mode',
139
+ code=ErrorCode.CONFIG_INVALID_CLUSTER_CAP,
140
+ notes=[
141
+ f'cluster_wide_cap={self.cluster_wide_cap}',
142
+ f'prefetch_buffer={self.prefetch_buffer}',
143
+ 'cluster_wide_cap requires hard cap mode (prefetch_buffer=0)',
144
+ ],
145
+ help_text='set prefetch_buffer=0 when using cluster_wide_cap',
146
+ )
147
+ )
125
148
 
126
149
  raise_collected(report)
127
150
  return self
@@ -2,7 +2,12 @@
2
2
  from __future__ import annotations
3
3
  from typing import Annotated, Self
4
4
  from pydantic import BaseModel, Field, model_validator
5
- from horsies.core.errors import ConfigurationError, ErrorCode, ValidationReport, raise_collected
5
+ from horsies.core.errors import (
6
+ ConfigurationError,
7
+ ErrorCode,
8
+ ValidationReport,
9
+ raise_collected,
10
+ )
6
11
 
7
12
 
8
13
  class RecoveryConfig(BaseModel):
@@ -73,29 +78,33 @@ class RecoveryConfig(BaseModel):
73
78
 
74
79
  # Validate runner heartbeat vs running stale threshold
75
80
  if self.running_stale_threshold_ms < min_running:
76
- report.add(ConfigurationError(
77
- message='running_stale_threshold_ms too low',
78
- code=ErrorCode.CONFIG_INVALID_RECOVERY,
79
- notes=[
80
- f'running_stale_threshold_ms={self.running_stale_threshold_ms}ms ({self.running_stale_threshold_ms/1000:.1f}s)',
81
- f'runner_heartbeat_interval_ms={self.runner_heartbeat_interval_ms}ms ({self.runner_heartbeat_interval_ms/1000:.1f}s)',
82
- 'threshold must be at least 2x heartbeat interval',
83
- ],
84
- help_text=f'set running_stale_threshold_ms >= {min_running}ms ({min_running/1000:.1f}s)',
85
- ))
81
+ report.add(
82
+ ConfigurationError(
83
+ message='running_stale_threshold_ms too low',
84
+ code=ErrorCode.CONFIG_INVALID_RECOVERY,
85
+ notes=[
86
+ f'running_stale_threshold_ms={self.running_stale_threshold_ms}ms ({self.running_stale_threshold_ms/1000:.1f}s)',
87
+ f'runner_heartbeat_interval_ms={self.runner_heartbeat_interval_ms}ms ({self.runner_heartbeat_interval_ms/1000:.1f}s)',
88
+ 'threshold must be at least 2x heartbeat interval',
89
+ ],
90
+ help_text=f'set running_stale_threshold_ms >= {min_running}ms ({min_running/1000:.1f}s)',
91
+ )
92
+ )
86
93
 
87
94
  # Validate claimer heartbeat vs claimed stale threshold
88
95
  if self.claimed_stale_threshold_ms < min_claimed:
89
- report.add(ConfigurationError(
90
- message='claimed_stale_threshold_ms too low',
91
- code=ErrorCode.CONFIG_INVALID_RECOVERY,
92
- notes=[
93
- f'claimed_stale_threshold_ms={self.claimed_stale_threshold_ms}ms ({self.claimed_stale_threshold_ms/1000:.1f}s)',
94
- f'claimer_heartbeat_interval_ms={self.claimer_heartbeat_interval_ms}ms ({self.claimer_heartbeat_interval_ms/1000:.1f}s)',
95
- 'threshold must be at least 2x heartbeat interval',
96
- ],
97
- help_text=f'set claimed_stale_threshold_ms >= {min_claimed}ms ({min_claimed/1000:.1f}s)',
98
- ))
96
+ report.add(
97
+ ConfigurationError(
98
+ message='claimed_stale_threshold_ms too low',
99
+ code=ErrorCode.CONFIG_INVALID_RECOVERY,
100
+ notes=[
101
+ f'claimed_stale_threshold_ms={self.claimed_stale_threshold_ms}ms ({self.claimed_stale_threshold_ms/1000:.1f}s)',
102
+ f'claimer_heartbeat_interval_ms={self.claimer_heartbeat_interval_ms}ms ({self.claimer_heartbeat_interval_ms/1000:.1f}s)',
103
+ 'threshold must be at least 2x heartbeat interval',
104
+ ],
105
+ help_text=f'set claimed_stale_threshold_ms >= {min_claimed}ms ({min_claimed/1000:.1f}s)',
106
+ )
107
+ )
99
108
 
100
109
  raise_collected(report)
101
110
  return self
@@ -5,7 +5,12 @@ from typing import Literal, Union, Optional, Any
5
5
  from pydantic import BaseModel, Field, model_validator
6
6
  from typing_extensions import Self
7
7
  from enum import Enum
8
- from horsies.core.errors import ConfigurationError, ErrorCode, ValidationReport, raise_collected
8
+ from horsies.core.errors import (
9
+ ConfigurationError,
10
+ ErrorCode,
11
+ ValidationReport,
12
+ raise_collected,
13
+ )
9
14
 
10
15
 
11
16
  class Weekday(str, Enum):
@@ -54,12 +59,14 @@ class IntervalSchedule(BaseModel):
54
59
  """Ensure at least one time unit is specified."""
55
60
  report = ValidationReport('schedule')
56
61
  if not any([self.seconds, self.minutes, self.hours, self.days]):
57
- report.add(ConfigurationError(
58
- message='IntervalSchedule requires at least one time unit',
59
- code=ErrorCode.CONFIG_INVALID_SCHEDULE,
60
- notes=['all time units (seconds, minutes, hours, days) are None'],
61
- help_text='specify at least one: seconds, minutes, hours, or days',
62
- ))
62
+ report.add(
63
+ ConfigurationError(
64
+ message='IntervalSchedule requires at least one time unit',
65
+ code=ErrorCode.CONFIG_INVALID_SCHEDULE,
66
+ notes=['all time units (seconds, minutes, hours, days) are None'],
67
+ help_text='specify at least one: seconds, minutes, hours, or days',
68
+ )
69
+ )
63
70
  raise_collected(report)
64
71
  return self
65
72
 
@@ -126,12 +133,14 @@ class WeeklySchedule(BaseModel):
126
133
  """Ensure no duplicate days."""
127
134
  report = ValidationReport('schedule')
128
135
  if len(self.days) != len(set(self.days)):
129
- report.add(ConfigurationError(
130
- message='WeeklySchedule has duplicate days',
131
- code=ErrorCode.CONFIG_INVALID_SCHEDULE,
132
- notes=[f'days: {[d.value for d in self.days]}'],
133
- help_text='each day should appear only once in the list',
134
- ))
136
+ report.add(
137
+ ConfigurationError(
138
+ message='WeeklySchedule has duplicate days',
139
+ code=ErrorCode.CONFIG_INVALID_SCHEDULE,
140
+ notes=[f'days: {[d.value for d in self.days]}'],
141
+ help_text='each day should appear only once in the list',
142
+ )
143
+ )
135
144
  raise_collected(report)
136
145
  return self
137
146
 
@@ -219,11 +228,13 @@ class ScheduleConfig(BaseModel):
219
228
  report = ValidationReport('schedule')
220
229
  names: list[str] = [s.name for s in self.schedules]
221
230
  if len(names) != len(set(names)):
222
- report.add(ConfigurationError(
223
- message='duplicate schedule names',
224
- code=ErrorCode.CONFIG_INVALID_SCHEDULE,
225
- notes=[f'schedule names: {names}'],
226
- help_text='each schedule must have a unique name',
227
- ))
231
+ report.add(
232
+ ConfigurationError(
233
+ message='duplicate schedule names',
234
+ code=ErrorCode.CONFIG_INVALID_SCHEDULE,
235
+ notes=[f'schedule names: {names}'],
236
+ help_text='each schedule must have a unique name',
237
+ )
238
+ )
228
239
  raise_collected(report)
229
240
  return self
@@ -22,6 +22,7 @@ if TYPE_CHECKING:
22
22
  from horsies.core.models.workflow import SubWorkflowSummary
23
23
 
24
24
  from horsies.core.types.status import TaskStatus
25
+
25
26
  T = TypeVar('T') # success payload
26
27
  E = TypeVar('E') # error payload (TaskError )
27
28