spakky-task 6.0.0__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.
- spakky_task-6.0.0/PKG-INFO +147 -0
- spakky_task-6.0.0/README.md +136 -0
- spakky_task-6.0.0/pyproject.toml +69 -0
- spakky_task-6.0.0/src/spakky/task/__init__.py +42 -0
- spakky_task-6.0.0/src/spakky/task/error.py +29 -0
- spakky_task-6.0.0/src/spakky/task/interfaces/__init__.py +0 -0
- spakky_task-6.0.0/src/spakky/task/interfaces/task_result.py +30 -0
- spakky_task-6.0.0/src/spakky/task/main.py +14 -0
- spakky_task-6.0.0/src/spakky/task/post_processor.py +58 -0
- spakky_task-6.0.0/src/spakky/task/py.typed +0 -0
- spakky_task-6.0.0/src/spakky/task/stereotype/__init__.py +1 -0
- spakky_task-6.0.0/src/spakky/task/stereotype/crontab.py +67 -0
- spakky_task-6.0.0/src/spakky/task/stereotype/schedule.py +76 -0
- spakky_task-6.0.0/src/spakky/task/stereotype/task_handler.py +55 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: spakky-task
|
|
3
|
+
Version: 6.0.0
|
|
4
|
+
Summary: Task queue abstraction for Spakky Framework (@TaskHandler, @task)
|
|
5
|
+
Author: Spakky
|
|
6
|
+
Author-email: Spakky <sejong418@icloud.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Requires-Dist: spakky>=6.0.0
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
|
|
12
|
+
# spakky-task
|
|
13
|
+
|
|
14
|
+
Task queue abstraction layer for [Spakky Framework](https://github.com/E5resso/spakky-framework).
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install spakky-task
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Features
|
|
23
|
+
|
|
24
|
+
- **`@TaskHandler` stereotype**: Marks classes as task handler pods
|
|
25
|
+
- **`@task` decorator**: Marks methods as on-demand dispatchable tasks
|
|
26
|
+
- **`@schedule` decorator**: Marks methods for periodic execution (interval, daily, crontab)
|
|
27
|
+
- **`Crontab` value object**: Python-native cron specification with `Weekday`/`Month` enums
|
|
28
|
+
- **Post-processor**: Automatically scans and registers task routes from `@TaskHandler` pods
|
|
29
|
+
- **Implementation-agnostic**: Works with any task queue backend (Celery, etc.) via plugins
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
### On-Demand Tasks
|
|
34
|
+
|
|
35
|
+
`@task` marks methods for on-demand dispatch. The backend plugin (e.g., `spakky-celery`)
|
|
36
|
+
intercepts calls via AOP and routes them to the task queue.
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from spakky.task import TaskHandler, task
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@TaskHandler()
|
|
43
|
+
class EmailTaskHandler:
|
|
44
|
+
@task
|
|
45
|
+
def send_email(self, to: str, subject: str, body: str) -> None:
|
|
46
|
+
"""Dispatched to the task queue when called."""
|
|
47
|
+
...
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Scheduled Tasks
|
|
51
|
+
|
|
52
|
+
`@schedule` marks methods for periodic execution. Exactly one of `interval`, `at`, or `crontab`
|
|
53
|
+
must be specified.
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
from datetime import time, timedelta
|
|
57
|
+
|
|
58
|
+
from spakky.task import TaskHandler, Crontab, Weekday, schedule
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@TaskHandler()
|
|
62
|
+
class MaintenanceHandler:
|
|
63
|
+
@schedule(interval=timedelta(minutes=30))
|
|
64
|
+
def health_check(self) -> None:
|
|
65
|
+
"""Runs every 30 minutes."""
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
@schedule(at=time(3, 0))
|
|
69
|
+
def daily_cleanup(self) -> None:
|
|
70
|
+
"""Runs daily at 03:00."""
|
|
71
|
+
...
|
|
72
|
+
|
|
73
|
+
@schedule(crontab=Crontab(weekday=Weekday.MONDAY, hour=9))
|
|
74
|
+
def weekly_report(self) -> None:
|
|
75
|
+
"""Runs every Monday at 09:00."""
|
|
76
|
+
...
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Crontab Specification
|
|
80
|
+
|
|
81
|
+
`Crontab` uses Python-native types instead of cron strings. `None` means "every" (wildcard).
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
from spakky.task import Crontab, Weekday, Month
|
|
85
|
+
|
|
86
|
+
# Every Monday at 03:00
|
|
87
|
+
Crontab(weekday=Weekday.MONDAY, hour=3)
|
|
88
|
+
|
|
89
|
+
# Mon/Wed/Fri at 09:00
|
|
90
|
+
Crontab(weekday=(Weekday.MONDAY, Weekday.WEDNESDAY, Weekday.FRIDAY), hour=9)
|
|
91
|
+
|
|
92
|
+
# 1st and 15th of every month at midnight
|
|
93
|
+
Crontab(day=(1, 15))
|
|
94
|
+
|
|
95
|
+
# Every January 1st at midnight
|
|
96
|
+
Crontab(month=Month.JANUARY, day=1)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Field order** (descending temporal granularity):
|
|
100
|
+
|
|
101
|
+
| Field | Type | Default |
|
|
102
|
+
|-----------|-----------------------------------|---------|
|
|
103
|
+
| `month` | `Month \| tuple[Month, ...] \| None` | `None` (every) |
|
|
104
|
+
| `day` | `int \| tuple[int, ...] \| None` | `None` (every) |
|
|
105
|
+
| `weekday` | `Weekday \| tuple[Weekday, ...] \| None` | `None` (every) |
|
|
106
|
+
| `hour` | `int` | `0` |
|
|
107
|
+
| `minute` | `int` | `0` |
|
|
108
|
+
|
|
109
|
+
### Accessing Task Routes
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from spakky.task import TaskRegistrationPostProcessor
|
|
113
|
+
|
|
114
|
+
post_processor = container.get(TaskRegistrationPostProcessor)
|
|
115
|
+
routes = post_processor.get_task_routes()
|
|
116
|
+
# {<bound method send_email>: TaskRoute(), ...}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Components
|
|
120
|
+
|
|
121
|
+
| Component | Description |
|
|
122
|
+
|-----------|-------------|
|
|
123
|
+
| `TaskHandler` | Stereotype decorator for task handler classes |
|
|
124
|
+
| `@task` | Method decorator for on-demand task dispatch |
|
|
125
|
+
| `@schedule` | Method decorator for periodic execution (`interval`, `at`, `crontab`) |
|
|
126
|
+
| `TaskRoute` | Annotation for `@task` methods |
|
|
127
|
+
| `ScheduleRoute` | Annotation for `@schedule` methods |
|
|
128
|
+
| `Crontab` | Frozen dataclass for cron-like schedule specification |
|
|
129
|
+
| `Weekday` | `IntEnum` for day of the week (Monday=0 ... Sunday=6) |
|
|
130
|
+
| `Month` | `IntEnum` for month of the year (January=1 ... December=12) |
|
|
131
|
+
| `TaskRegistrationPostProcessor` | Scans `@TaskHandler` pods and collects `@task` methods |
|
|
132
|
+
|
|
133
|
+
## Errors
|
|
134
|
+
|
|
135
|
+
| Error | Description |
|
|
136
|
+
|-------|-------------|
|
|
137
|
+
| `TaskNotFoundError` | Task reference not found in the registry |
|
|
138
|
+
| `DuplicateTaskError` | Attempting to register an already-registered task |
|
|
139
|
+
| `InvalidScheduleSpecificationError` | `@schedule` called with zero or multiple schedule options |
|
|
140
|
+
|
|
141
|
+
## Related Packages
|
|
142
|
+
|
|
143
|
+
- **`spakky-celery`**: Celery backend for task dispatch and schedule registration via AOP
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT License
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# spakky-task
|
|
2
|
+
|
|
3
|
+
Task queue abstraction layer for [Spakky Framework](https://github.com/E5resso/spakky-framework).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install spakky-task
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Features
|
|
12
|
+
|
|
13
|
+
- **`@TaskHandler` stereotype**: Marks classes as task handler pods
|
|
14
|
+
- **`@task` decorator**: Marks methods as on-demand dispatchable tasks
|
|
15
|
+
- **`@schedule` decorator**: Marks methods for periodic execution (interval, daily, crontab)
|
|
16
|
+
- **`Crontab` value object**: Python-native cron specification with `Weekday`/`Month` enums
|
|
17
|
+
- **Post-processor**: Automatically scans and registers task routes from `@TaskHandler` pods
|
|
18
|
+
- **Implementation-agnostic**: Works with any task queue backend (Celery, etc.) via plugins
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### On-Demand Tasks
|
|
23
|
+
|
|
24
|
+
`@task` marks methods for on-demand dispatch. The backend plugin (e.g., `spakky-celery`)
|
|
25
|
+
intercepts calls via AOP and routes them to the task queue.
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from spakky.task import TaskHandler, task
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@TaskHandler()
|
|
32
|
+
class EmailTaskHandler:
|
|
33
|
+
@task
|
|
34
|
+
def send_email(self, to: str, subject: str, body: str) -> None:
|
|
35
|
+
"""Dispatched to the task queue when called."""
|
|
36
|
+
...
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Scheduled Tasks
|
|
40
|
+
|
|
41
|
+
`@schedule` marks methods for periodic execution. Exactly one of `interval`, `at`, or `crontab`
|
|
42
|
+
must be specified.
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from datetime import time, timedelta
|
|
46
|
+
|
|
47
|
+
from spakky.task import TaskHandler, Crontab, Weekday, schedule
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@TaskHandler()
|
|
51
|
+
class MaintenanceHandler:
|
|
52
|
+
@schedule(interval=timedelta(minutes=30))
|
|
53
|
+
def health_check(self) -> None:
|
|
54
|
+
"""Runs every 30 minutes."""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
@schedule(at=time(3, 0))
|
|
58
|
+
def daily_cleanup(self) -> None:
|
|
59
|
+
"""Runs daily at 03:00."""
|
|
60
|
+
...
|
|
61
|
+
|
|
62
|
+
@schedule(crontab=Crontab(weekday=Weekday.MONDAY, hour=9))
|
|
63
|
+
def weekly_report(self) -> None:
|
|
64
|
+
"""Runs every Monday at 09:00."""
|
|
65
|
+
...
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Crontab Specification
|
|
69
|
+
|
|
70
|
+
`Crontab` uses Python-native types instead of cron strings. `None` means "every" (wildcard).
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from spakky.task import Crontab, Weekday, Month
|
|
74
|
+
|
|
75
|
+
# Every Monday at 03:00
|
|
76
|
+
Crontab(weekday=Weekday.MONDAY, hour=3)
|
|
77
|
+
|
|
78
|
+
# Mon/Wed/Fri at 09:00
|
|
79
|
+
Crontab(weekday=(Weekday.MONDAY, Weekday.WEDNESDAY, Weekday.FRIDAY), hour=9)
|
|
80
|
+
|
|
81
|
+
# 1st and 15th of every month at midnight
|
|
82
|
+
Crontab(day=(1, 15))
|
|
83
|
+
|
|
84
|
+
# Every January 1st at midnight
|
|
85
|
+
Crontab(month=Month.JANUARY, day=1)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Field order** (descending temporal granularity):
|
|
89
|
+
|
|
90
|
+
| Field | Type | Default |
|
|
91
|
+
|-----------|-----------------------------------|---------|
|
|
92
|
+
| `month` | `Month \| tuple[Month, ...] \| None` | `None` (every) |
|
|
93
|
+
| `day` | `int \| tuple[int, ...] \| None` | `None` (every) |
|
|
94
|
+
| `weekday` | `Weekday \| tuple[Weekday, ...] \| None` | `None` (every) |
|
|
95
|
+
| `hour` | `int` | `0` |
|
|
96
|
+
| `minute` | `int` | `0` |
|
|
97
|
+
|
|
98
|
+
### Accessing Task Routes
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from spakky.task import TaskRegistrationPostProcessor
|
|
102
|
+
|
|
103
|
+
post_processor = container.get(TaskRegistrationPostProcessor)
|
|
104
|
+
routes = post_processor.get_task_routes()
|
|
105
|
+
# {<bound method send_email>: TaskRoute(), ...}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Components
|
|
109
|
+
|
|
110
|
+
| Component | Description |
|
|
111
|
+
|-----------|-------------|
|
|
112
|
+
| `TaskHandler` | Stereotype decorator for task handler classes |
|
|
113
|
+
| `@task` | Method decorator for on-demand task dispatch |
|
|
114
|
+
| `@schedule` | Method decorator for periodic execution (`interval`, `at`, `crontab`) |
|
|
115
|
+
| `TaskRoute` | Annotation for `@task` methods |
|
|
116
|
+
| `ScheduleRoute` | Annotation for `@schedule` methods |
|
|
117
|
+
| `Crontab` | Frozen dataclass for cron-like schedule specification |
|
|
118
|
+
| `Weekday` | `IntEnum` for day of the week (Monday=0 ... Sunday=6) |
|
|
119
|
+
| `Month` | `IntEnum` for month of the year (January=1 ... December=12) |
|
|
120
|
+
| `TaskRegistrationPostProcessor` | Scans `@TaskHandler` pods and collects `@task` methods |
|
|
121
|
+
|
|
122
|
+
## Errors
|
|
123
|
+
|
|
124
|
+
| Error | Description |
|
|
125
|
+
|-------|-------------|
|
|
126
|
+
| `TaskNotFoundError` | Task reference not found in the registry |
|
|
127
|
+
| `DuplicateTaskError` | Attempting to register an already-registered task |
|
|
128
|
+
| `InvalidScheduleSpecificationError` | `@schedule` called with zero or multiple schedule options |
|
|
129
|
+
|
|
130
|
+
## Related Packages
|
|
131
|
+
|
|
132
|
+
- **`spakky-celery`**: Celery backend for task dispatch and schedule registration via AOP
|
|
133
|
+
|
|
134
|
+
## License
|
|
135
|
+
|
|
136
|
+
MIT License
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "spakky-task"
|
|
3
|
+
version = "6.0.0"
|
|
4
|
+
description = "Task queue abstraction for Spakky Framework (@TaskHandler, @task)"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "Spakky", email = "sejong418@icloud.com" }]
|
|
9
|
+
dependencies = ["spakky>=6.0.0"]
|
|
10
|
+
|
|
11
|
+
[project.entry-points."spakky.plugins"]
|
|
12
|
+
spakky-task = "spakky.task.main:initialize"
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["uv_build>=0.10.10,<0.11.0"]
|
|
16
|
+
build-backend = "uv_build"
|
|
17
|
+
|
|
18
|
+
[tool.uv.build-backend]
|
|
19
|
+
module-root = "src"
|
|
20
|
+
module-name = "spakky.task"
|
|
21
|
+
|
|
22
|
+
[tool.pyrefly]
|
|
23
|
+
python-version = "3.14"
|
|
24
|
+
search_path = ["src", "."]
|
|
25
|
+
project_excludes = ["**/__pycache__", "**/*.pyc"]
|
|
26
|
+
|
|
27
|
+
[tool.ruff]
|
|
28
|
+
builtins = ["_"]
|
|
29
|
+
cache-dir = "~/.cache/ruff"
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
pythonpath = "src/spakky/task"
|
|
33
|
+
testpaths = "tests"
|
|
34
|
+
python_files = ["test_*.py"]
|
|
35
|
+
asyncio_mode = "auto"
|
|
36
|
+
addopts = """
|
|
37
|
+
--cov
|
|
38
|
+
--cov-report=term
|
|
39
|
+
--cov-report=xml
|
|
40
|
+
--no-cov-on-fail
|
|
41
|
+
--strict-markers
|
|
42
|
+
--dist=load
|
|
43
|
+
-p no:warnings
|
|
44
|
+
-n auto
|
|
45
|
+
--spec
|
|
46
|
+
"""
|
|
47
|
+
spec_test_format = "{result} {docstring_summary}"
|
|
48
|
+
|
|
49
|
+
[tool.coverage.run]
|
|
50
|
+
include = ["src/spakky/task/**/*.py"]
|
|
51
|
+
branch = true
|
|
52
|
+
|
|
53
|
+
[tool.coverage.report]
|
|
54
|
+
show_missing = true
|
|
55
|
+
precision = 2
|
|
56
|
+
fail_under = 90
|
|
57
|
+
skip_empty = true
|
|
58
|
+
exclude_lines = [
|
|
59
|
+
"pragma: no cover",
|
|
60
|
+
"def __repr__",
|
|
61
|
+
"raise AssertionError",
|
|
62
|
+
"raise NotImplementedError",
|
|
63
|
+
"@(abc\\.)?abstractmethod",
|
|
64
|
+
"@(typing\\.)?overload",
|
|
65
|
+
"\\.\\.\\.",
|
|
66
|
+
"pass",
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Spakky Task package - Task queue abstraction support."""
|
|
2
|
+
|
|
3
|
+
from spakky.core.application.plugin import Plugin
|
|
4
|
+
|
|
5
|
+
from spakky.task.error import (
|
|
6
|
+
AbstractSpakkyTaskError,
|
|
7
|
+
DuplicateTaskError,
|
|
8
|
+
InvalidScheduleSpecificationError,
|
|
9
|
+
TaskNotFoundError,
|
|
10
|
+
)
|
|
11
|
+
from spakky.task.post_processor import TaskRegistrationPostProcessor
|
|
12
|
+
from spakky.task.stereotype.crontab import Crontab, Month, Weekday
|
|
13
|
+
from spakky.task.stereotype.schedule import ScheduleRoute, schedule
|
|
14
|
+
from spakky.task.stereotype.task_handler import (
|
|
15
|
+
TaskHandler,
|
|
16
|
+
TaskRoute,
|
|
17
|
+
task,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
PLUGIN_NAME = Plugin(name="spakky-task")
|
|
21
|
+
"""Plugin identifier for the Spakky Task package."""
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
# Stereotype
|
|
25
|
+
"TaskHandler",
|
|
26
|
+
"TaskRoute",
|
|
27
|
+
"task",
|
|
28
|
+
"Crontab",
|
|
29
|
+
"Weekday",
|
|
30
|
+
"Month",
|
|
31
|
+
"ScheduleRoute",
|
|
32
|
+
"schedule",
|
|
33
|
+
# Post-Processors
|
|
34
|
+
"TaskRegistrationPostProcessor",
|
|
35
|
+
# Errors
|
|
36
|
+
"AbstractSpakkyTaskError",
|
|
37
|
+
"TaskNotFoundError",
|
|
38
|
+
"DuplicateTaskError",
|
|
39
|
+
"InvalidScheduleSpecificationError",
|
|
40
|
+
# Plugin
|
|
41
|
+
"PLUGIN_NAME",
|
|
42
|
+
]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Spakky Task error hierarchy."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC
|
|
4
|
+
|
|
5
|
+
from spakky.core.common.error import AbstractSpakkyFrameworkError
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class AbstractSpakkyTaskError(AbstractSpakkyFrameworkError, ABC):
|
|
9
|
+
"""Base class for all spakky-task errors."""
|
|
10
|
+
|
|
11
|
+
...
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TaskNotFoundError(AbstractSpakkyTaskError):
|
|
15
|
+
"""Raised when a task reference cannot be found in the registry."""
|
|
16
|
+
|
|
17
|
+
message = "Task not found in the registry"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DuplicateTaskError(AbstractSpakkyTaskError):
|
|
21
|
+
"""Raised when attempting to register a task that already exists."""
|
|
22
|
+
|
|
23
|
+
message = "Duplicate task registered"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class InvalidScheduleSpecificationError(AbstractSpakkyTaskError):
|
|
27
|
+
"""Raised when a ScheduleRoute has invalid schedule options."""
|
|
28
|
+
|
|
29
|
+
message = "Exactly one of 'interval', 'at', or 'crontab' must be provided"
|
|
File without changes
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Abstract task result handle for background task dispatchers."""
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from typing import Generic, TypeVar
|
|
5
|
+
|
|
6
|
+
from spakky.core.common.interfaces.equatable import IEquatable
|
|
7
|
+
|
|
8
|
+
T = TypeVar("T")
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class AbstractTaskResult(ABC, Generic[T]):
|
|
12
|
+
"""Abstract handle for the result of a dispatched background task.
|
|
13
|
+
|
|
14
|
+
Concrete adapters (e.g. CeleryTaskResult) implement this for each broker.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
@abstractmethod
|
|
19
|
+
def task_id(self) -> IEquatable:
|
|
20
|
+
"""Unique identifier for the dispatched task."""
|
|
21
|
+
...
|
|
22
|
+
|
|
23
|
+
@abstractmethod
|
|
24
|
+
def get(self) -> T:
|
|
25
|
+
"""Block until the task completes and return its result.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
The return value of the executed task method.
|
|
29
|
+
"""
|
|
30
|
+
...
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Plugin initialization entry point."""
|
|
2
|
+
|
|
3
|
+
from spakky.core.application.application import SpakkyApplication
|
|
4
|
+
|
|
5
|
+
from spakky.task.post_processor import TaskRegistrationPostProcessor
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def initialize(app: SpakkyApplication) -> None:
|
|
9
|
+
"""Initialize the spakky-task plugin.
|
|
10
|
+
|
|
11
|
+
Args:
|
|
12
|
+
app: The SpakkyApplication instance.
|
|
13
|
+
"""
|
|
14
|
+
app.add(TaskRegistrationPostProcessor)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Task handler registration post-processor."""
|
|
2
|
+
|
|
3
|
+
from inspect import getmembers, ismethod
|
|
4
|
+
from logging import getLogger
|
|
5
|
+
|
|
6
|
+
from spakky.core.common.types import Func
|
|
7
|
+
from spakky.core.pod.annotations.pod import Pod
|
|
8
|
+
from spakky.core.pod.interfaces.post_processor import IPostProcessor
|
|
9
|
+
|
|
10
|
+
from spakky.task.stereotype.task_handler import TaskHandler, TaskRoute
|
|
11
|
+
|
|
12
|
+
logger = getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@Pod()
|
|
16
|
+
class TaskRegistrationPostProcessor(IPostProcessor):
|
|
17
|
+
"""Post-processor that scans @TaskHandler pods for @task methods.
|
|
18
|
+
|
|
19
|
+
This post-processor collects all task routes from TaskHandler pods
|
|
20
|
+
and makes them available for task queue implementations to register.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
_task_routes: dict[Func, TaskRoute]
|
|
24
|
+
|
|
25
|
+
def __init__(self) -> None:
|
|
26
|
+
self._task_routes = {}
|
|
27
|
+
|
|
28
|
+
def post_process(self, pod: object) -> object:
|
|
29
|
+
"""Scan pod for @task methods and register their routes.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
pod: The pod instance to process.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
The unmodified pod instance.
|
|
36
|
+
"""
|
|
37
|
+
pod_type = type(pod)
|
|
38
|
+
|
|
39
|
+
if not TaskHandler.exists(pod_type):
|
|
40
|
+
return pod
|
|
41
|
+
|
|
42
|
+
for name, method in getmembers(pod, predicate=ismethod):
|
|
43
|
+
route = TaskRoute.get_or_none(method)
|
|
44
|
+
if route is None:
|
|
45
|
+
continue
|
|
46
|
+
|
|
47
|
+
self._task_routes[method] = route
|
|
48
|
+
logger.debug(f"Registered task {pod_type.__name__}.{name}")
|
|
49
|
+
|
|
50
|
+
return pod
|
|
51
|
+
|
|
52
|
+
def get_task_routes(self) -> dict[Func, TaskRoute]:
|
|
53
|
+
"""Get all registered task routes.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Dictionary mapping task methods to their routes.
|
|
57
|
+
"""
|
|
58
|
+
return self._task_routes.copy()
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Task handler stereotype module."""
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Crontab value object for schedule specification."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import IntEnum
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Weekday(IntEnum):
|
|
8
|
+
"""Day of the week (ISO 8601: Monday=0)."""
|
|
9
|
+
|
|
10
|
+
MONDAY = 0
|
|
11
|
+
TUESDAY = 1
|
|
12
|
+
WEDNESDAY = 2
|
|
13
|
+
THURSDAY = 3
|
|
14
|
+
FRIDAY = 4
|
|
15
|
+
SATURDAY = 5
|
|
16
|
+
SUNDAY = 6
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Month(IntEnum):
|
|
20
|
+
"""Month of the year (1-12)."""
|
|
21
|
+
|
|
22
|
+
JANUARY = 1
|
|
23
|
+
FEBRUARY = 2
|
|
24
|
+
MARCH = 3
|
|
25
|
+
APRIL = 4
|
|
26
|
+
MAY = 5
|
|
27
|
+
JUNE = 6
|
|
28
|
+
JULY = 7
|
|
29
|
+
AUGUST = 8
|
|
30
|
+
SEPTEMBER = 9
|
|
31
|
+
OCTOBER = 10
|
|
32
|
+
NOVEMBER = 11
|
|
33
|
+
DECEMBER = 12
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class Crontab:
|
|
38
|
+
"""Cron-like schedule specification using Python native types.
|
|
39
|
+
|
|
40
|
+
Fields use ``None`` to mean "every" (wildcard).
|
|
41
|
+
A single ``int`` means exactly that value; a ``tuple`` means multiple values.
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
# Every Monday at 03:00
|
|
45
|
+
Crontab(weekday=Weekday.MONDAY, hour=3)
|
|
46
|
+
|
|
47
|
+
# Mon/Wed/Fri at 09:00
|
|
48
|
+
Crontab(weekday=(Weekday.MONDAY, Weekday.WEDNESDAY, Weekday.FRIDAY), hour=9)
|
|
49
|
+
|
|
50
|
+
# 1st and 15th of every month at midnight
|
|
51
|
+
Crontab(day=(1, 15))
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
month: Month | tuple[Month, ...] | None = None
|
|
55
|
+
"""Month of the year. None means every month."""
|
|
56
|
+
|
|
57
|
+
day: int | tuple[int, ...] | None = None
|
|
58
|
+
"""Day of the month (1-31). None means every day."""
|
|
59
|
+
|
|
60
|
+
weekday: Weekday | tuple[Weekday, ...] | None = None
|
|
61
|
+
"""Day of the week. None means every day."""
|
|
62
|
+
|
|
63
|
+
hour: int = 0
|
|
64
|
+
"""Hour of the day (0-23)."""
|
|
65
|
+
|
|
66
|
+
minute: int = 0
|
|
67
|
+
"""Minute of the hour (0-59)."""
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Schedule stereotype for periodic task execution.
|
|
2
|
+
|
|
3
|
+
Provides @schedule decorator to mark TaskHandler methods for
|
|
4
|
+
periodic execution (interval, daily, or crontab-based).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from datetime import time, timedelta
|
|
9
|
+
from typing import Callable, ParamSpec, TypeVar, cast
|
|
10
|
+
|
|
11
|
+
from spakky.core.common.annotation import FunctionAnnotation
|
|
12
|
+
|
|
13
|
+
from spakky.task.error import InvalidScheduleSpecificationError
|
|
14
|
+
from spakky.task.stereotype.crontab import Crontab
|
|
15
|
+
|
|
16
|
+
P = ParamSpec("P")
|
|
17
|
+
T = TypeVar("T")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class ScheduleRoute(FunctionAnnotation):
|
|
22
|
+
"""Annotation for marking methods as periodically scheduled tasks.
|
|
23
|
+
|
|
24
|
+
Exactly one of ``interval``, ``at``, or ``crontab`` must be provided.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
interval: timedelta | None = None
|
|
28
|
+
"""Fixed interval between executions."""
|
|
29
|
+
|
|
30
|
+
at: time | None = None
|
|
31
|
+
"""Daily execution at a specific time."""
|
|
32
|
+
|
|
33
|
+
crontab: Crontab | None = None
|
|
34
|
+
"""Cron-like schedule specification."""
|
|
35
|
+
|
|
36
|
+
def __post_init__(self) -> None:
|
|
37
|
+
specified = sum(x is not None for x in (self.interval, self.at, self.crontab))
|
|
38
|
+
if specified != 1:
|
|
39
|
+
raise InvalidScheduleSpecificationError()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def schedule(
|
|
43
|
+
*,
|
|
44
|
+
interval: timedelta | None = None,
|
|
45
|
+
at: time | None = None,
|
|
46
|
+
crontab: Crontab | None = None,
|
|
47
|
+
) -> Callable[[Callable[P, T]], Callable[P, T]]:
|
|
48
|
+
"""Decorator for marking methods as periodically scheduled tasks.
|
|
49
|
+
|
|
50
|
+
Exactly one of ``interval``, ``at``, or ``crontab`` must be specified.
|
|
51
|
+
|
|
52
|
+
Example:
|
|
53
|
+
@TaskHandler()
|
|
54
|
+
class MaintenanceHandler:
|
|
55
|
+
@schedule(interval=timedelta(minutes=30))
|
|
56
|
+
def health_check(self) -> None:
|
|
57
|
+
...
|
|
58
|
+
|
|
59
|
+
@schedule(at=time(3, 0))
|
|
60
|
+
def daily_cleanup(self) -> None:
|
|
61
|
+
...
|
|
62
|
+
|
|
63
|
+
@schedule(crontab=Crontab(hour=9, weekday=(Weekday.MONDAY, Weekday.WEDNESDAY, Weekday.FRIDAY)))
|
|
64
|
+
def triweekly_report(self) -> None:
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
interval: Fixed interval between executions.
|
|
69
|
+
at: Daily execution at a specific time.
|
|
70
|
+
crontab: Cron-like schedule specification.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
A decorator that annotates the method with ScheduleRoute.
|
|
74
|
+
"""
|
|
75
|
+
route = ScheduleRoute(interval=interval, at=at, crontab=crontab)
|
|
76
|
+
return cast(Callable[[Callable[P, T]], Callable[P, T]], route)
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""TaskHandler stereotype and task routing decorators.
|
|
2
|
+
|
|
3
|
+
This module provides @TaskHandler stereotype and @task decorator
|
|
4
|
+
for organizing task-queue-driven architectures.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from typing import Callable, ParamSpec, TypeVar, cast
|
|
9
|
+
|
|
10
|
+
from spakky.core.common.annotation import FunctionAnnotation
|
|
11
|
+
from spakky.core.pod.annotations.pod import Pod
|
|
12
|
+
|
|
13
|
+
P = ParamSpec("P")
|
|
14
|
+
T = TypeVar("T")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class TaskRoute(FunctionAnnotation):
|
|
19
|
+
"""Annotation for marking methods as dispatchable tasks.
|
|
20
|
+
|
|
21
|
+
Associates a method as a task that can be dispatched to a task queue.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def task(obj: Callable[P, T]) -> Callable[P, T]:
|
|
26
|
+
"""Decorator for marking methods as dispatchable tasks.
|
|
27
|
+
|
|
28
|
+
All @task methods are dispatched to the task queue by the plugin aspect.
|
|
29
|
+
|
|
30
|
+
Example:
|
|
31
|
+
@TaskHandler()
|
|
32
|
+
class EmailTaskHandler:
|
|
33
|
+
@task
|
|
34
|
+
def send_email(self, to: str, subject: str, body: str) -> None:
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
obj: The method to mark as a task.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The annotated method.
|
|
42
|
+
"""
|
|
43
|
+
route = TaskRoute()
|
|
44
|
+
return cast(Callable[P, T], route(obj))
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass(eq=False)
|
|
48
|
+
class TaskHandler(Pod):
|
|
49
|
+
"""Stereotype for task handler classes.
|
|
50
|
+
|
|
51
|
+
TaskHandlers contain methods decorated with @task that
|
|
52
|
+
can be dispatched to task queues asynchronously.
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
...
|