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.
@@ -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"
@@ -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
+ ...