ccproxy-api 0.1.0__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.
- ccproxy/__init__.py +4 -0
- ccproxy/__main__.py +7 -0
- ccproxy/_version.py +21 -0
- ccproxy/adapters/__init__.py +11 -0
- ccproxy/adapters/base.py +80 -0
- ccproxy/adapters/openai/__init__.py +43 -0
- ccproxy/adapters/openai/adapter.py +915 -0
- ccproxy/adapters/openai/models.py +412 -0
- ccproxy/adapters/openai/streaming.py +449 -0
- ccproxy/api/__init__.py +28 -0
- ccproxy/api/app.py +225 -0
- ccproxy/api/dependencies.py +140 -0
- ccproxy/api/middleware/__init__.py +11 -0
- ccproxy/api/middleware/auth.py +0 -0
- ccproxy/api/middleware/cors.py +55 -0
- ccproxy/api/middleware/errors.py +703 -0
- ccproxy/api/middleware/headers.py +51 -0
- ccproxy/api/middleware/logging.py +175 -0
- ccproxy/api/middleware/request_id.py +69 -0
- ccproxy/api/middleware/server_header.py +62 -0
- ccproxy/api/responses.py +84 -0
- ccproxy/api/routes/__init__.py +16 -0
- ccproxy/api/routes/claude.py +181 -0
- ccproxy/api/routes/health.py +489 -0
- ccproxy/api/routes/metrics.py +1033 -0
- ccproxy/api/routes/proxy.py +238 -0
- ccproxy/auth/__init__.py +75 -0
- ccproxy/auth/bearer.py +68 -0
- ccproxy/auth/credentials_adapter.py +93 -0
- ccproxy/auth/dependencies.py +229 -0
- ccproxy/auth/exceptions.py +79 -0
- ccproxy/auth/manager.py +102 -0
- ccproxy/auth/models.py +118 -0
- ccproxy/auth/oauth/__init__.py +26 -0
- ccproxy/auth/oauth/models.py +49 -0
- ccproxy/auth/oauth/routes.py +396 -0
- ccproxy/auth/oauth/storage.py +0 -0
- ccproxy/auth/storage/__init__.py +12 -0
- ccproxy/auth/storage/base.py +57 -0
- ccproxy/auth/storage/json_file.py +159 -0
- ccproxy/auth/storage/keyring.py +192 -0
- ccproxy/claude_sdk/__init__.py +20 -0
- ccproxy/claude_sdk/client.py +169 -0
- ccproxy/claude_sdk/converter.py +331 -0
- ccproxy/claude_sdk/options.py +120 -0
- ccproxy/cli/__init__.py +14 -0
- ccproxy/cli/commands/__init__.py +8 -0
- ccproxy/cli/commands/auth.py +553 -0
- ccproxy/cli/commands/config/__init__.py +14 -0
- ccproxy/cli/commands/config/commands.py +766 -0
- ccproxy/cli/commands/config/schema_commands.py +119 -0
- ccproxy/cli/commands/serve.py +630 -0
- ccproxy/cli/docker/__init__.py +34 -0
- ccproxy/cli/docker/adapter_factory.py +157 -0
- ccproxy/cli/docker/params.py +278 -0
- ccproxy/cli/helpers.py +144 -0
- ccproxy/cli/main.py +193 -0
- ccproxy/cli/options/__init__.py +14 -0
- ccproxy/cli/options/claude_options.py +216 -0
- ccproxy/cli/options/core_options.py +40 -0
- ccproxy/cli/options/security_options.py +48 -0
- ccproxy/cli/options/server_options.py +117 -0
- ccproxy/config/__init__.py +40 -0
- ccproxy/config/auth.py +154 -0
- ccproxy/config/claude.py +124 -0
- ccproxy/config/cors.py +79 -0
- ccproxy/config/discovery.py +87 -0
- ccproxy/config/docker_settings.py +265 -0
- ccproxy/config/loader.py +108 -0
- ccproxy/config/observability.py +158 -0
- ccproxy/config/pricing.py +88 -0
- ccproxy/config/reverse_proxy.py +31 -0
- ccproxy/config/scheduler.py +89 -0
- ccproxy/config/security.py +14 -0
- ccproxy/config/server.py +81 -0
- ccproxy/config/settings.py +534 -0
- ccproxy/config/validators.py +231 -0
- ccproxy/core/__init__.py +274 -0
- ccproxy/core/async_utils.py +675 -0
- ccproxy/core/constants.py +97 -0
- ccproxy/core/errors.py +256 -0
- ccproxy/core/http.py +328 -0
- ccproxy/core/http_transformers.py +428 -0
- ccproxy/core/interfaces.py +247 -0
- ccproxy/core/logging.py +189 -0
- ccproxy/core/middleware.py +114 -0
- ccproxy/core/proxy.py +143 -0
- ccproxy/core/system.py +38 -0
- ccproxy/core/transformers.py +259 -0
- ccproxy/core/types.py +129 -0
- ccproxy/core/validators.py +288 -0
- ccproxy/docker/__init__.py +67 -0
- ccproxy/docker/adapter.py +588 -0
- ccproxy/docker/docker_path.py +207 -0
- ccproxy/docker/middleware.py +103 -0
- ccproxy/docker/models.py +228 -0
- ccproxy/docker/protocol.py +192 -0
- ccproxy/docker/stream_process.py +264 -0
- ccproxy/docker/validators.py +173 -0
- ccproxy/models/__init__.py +123 -0
- ccproxy/models/errors.py +42 -0
- ccproxy/models/messages.py +243 -0
- ccproxy/models/requests.py +85 -0
- ccproxy/models/responses.py +227 -0
- ccproxy/models/types.py +102 -0
- ccproxy/observability/__init__.py +51 -0
- ccproxy/observability/access_logger.py +400 -0
- ccproxy/observability/context.py +447 -0
- ccproxy/observability/metrics.py +539 -0
- ccproxy/observability/pushgateway.py +366 -0
- ccproxy/observability/sse_events.py +303 -0
- ccproxy/observability/stats_printer.py +755 -0
- ccproxy/observability/storage/__init__.py +1 -0
- ccproxy/observability/storage/duckdb_simple.py +665 -0
- ccproxy/observability/storage/models.py +55 -0
- ccproxy/pricing/__init__.py +19 -0
- ccproxy/pricing/cache.py +212 -0
- ccproxy/pricing/loader.py +267 -0
- ccproxy/pricing/models.py +106 -0
- ccproxy/pricing/updater.py +309 -0
- ccproxy/scheduler/__init__.py +39 -0
- ccproxy/scheduler/core.py +335 -0
- ccproxy/scheduler/exceptions.py +34 -0
- ccproxy/scheduler/manager.py +186 -0
- ccproxy/scheduler/registry.py +150 -0
- ccproxy/scheduler/tasks.py +484 -0
- ccproxy/services/__init__.py +10 -0
- ccproxy/services/claude_sdk_service.py +614 -0
- ccproxy/services/credentials/__init__.py +55 -0
- ccproxy/services/credentials/config.py +105 -0
- ccproxy/services/credentials/manager.py +562 -0
- ccproxy/services/credentials/oauth_client.py +482 -0
- ccproxy/services/proxy_service.py +1536 -0
- ccproxy/static/.keep +0 -0
- ccproxy/testing/__init__.py +34 -0
- ccproxy/testing/config.py +148 -0
- ccproxy/testing/content_generation.py +197 -0
- ccproxy/testing/mock_responses.py +262 -0
- ccproxy/testing/response_handlers.py +161 -0
- ccproxy/testing/scenarios.py +241 -0
- ccproxy/utils/__init__.py +6 -0
- ccproxy/utils/cost_calculator.py +210 -0
- ccproxy/utils/streaming_metrics.py +199 -0
- ccproxy_api-0.1.0.dist-info/METADATA +253 -0
- ccproxy_api-0.1.0.dist-info/RECORD +148 -0
- ccproxy_api-0.1.0.dist-info/WHEEL +4 -0
- ccproxy_api-0.1.0.dist-info/entry_points.txt +2 -0
- ccproxy_api-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""Scheduler-specific exceptions."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SchedulerError(Exception):
|
|
5
|
+
"""Base exception for scheduler-related errors."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TaskRegistrationError(SchedulerError):
|
|
11
|
+
"""Raised when task registration fails."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TaskNotFoundError(SchedulerError):
|
|
17
|
+
"""Raised when attempting to access a task that doesn't exist."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TaskExecutionError(SchedulerError):
|
|
23
|
+
"""Raised when task execution encounters an error."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, task_name: str, original_error: Exception):
|
|
26
|
+
self.task_name = task_name
|
|
27
|
+
self.original_error = original_error
|
|
28
|
+
super().__init__(f"Task '{task_name}' execution failed: {original_error}")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SchedulerShutdownError(SchedulerError):
|
|
32
|
+
"""Raised when scheduler shutdown encounters an error."""
|
|
33
|
+
|
|
34
|
+
pass
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"""Scheduler management for FastAPI integration."""
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import structlog
|
|
7
|
+
|
|
8
|
+
from ccproxy.config.settings import Settings
|
|
9
|
+
|
|
10
|
+
from .core import Scheduler, get_scheduler
|
|
11
|
+
from .registry import register_task
|
|
12
|
+
from .tasks import PricingCacheUpdateTask, PushgatewayTask, StatsPrintingTask
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
logger = structlog.get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
async def setup_scheduler_tasks(scheduler: Scheduler, settings: Settings) -> None:
|
|
19
|
+
"""
|
|
20
|
+
Setup and configure all scheduler tasks based on settings.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
scheduler: Scheduler instance
|
|
24
|
+
settings: Application settings
|
|
25
|
+
"""
|
|
26
|
+
scheduler_config = settings.scheduler
|
|
27
|
+
|
|
28
|
+
if not scheduler_config.enabled:
|
|
29
|
+
logger.info("scheduler_disabled")
|
|
30
|
+
return
|
|
31
|
+
|
|
32
|
+
# Add pushgateway task if enabled
|
|
33
|
+
if scheduler_config.pushgateway_enabled:
|
|
34
|
+
try:
|
|
35
|
+
await scheduler.add_task(
|
|
36
|
+
task_name="pushgateway",
|
|
37
|
+
task_type="pushgateway",
|
|
38
|
+
interval_seconds=scheduler_config.pushgateway_interval_seconds,
|
|
39
|
+
enabled=True,
|
|
40
|
+
max_backoff_seconds=scheduler_config.pushgateway_max_backoff_seconds,
|
|
41
|
+
)
|
|
42
|
+
logger.info(
|
|
43
|
+
"pushgateway_task_added",
|
|
44
|
+
interval_seconds=scheduler_config.pushgateway_interval_seconds,
|
|
45
|
+
)
|
|
46
|
+
except Exception as e:
|
|
47
|
+
logger.error(
|
|
48
|
+
"pushgateway_task_add_failed",
|
|
49
|
+
error=str(e),
|
|
50
|
+
error_type=type(e).__name__,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Add stats printing task if enabled
|
|
54
|
+
if scheduler_config.stats_printing_enabled:
|
|
55
|
+
try:
|
|
56
|
+
await scheduler.add_task(
|
|
57
|
+
task_name="stats_printing",
|
|
58
|
+
task_type="stats_printing",
|
|
59
|
+
interval_seconds=scheduler_config.stats_printing_interval_seconds,
|
|
60
|
+
enabled=True,
|
|
61
|
+
)
|
|
62
|
+
logger.info(
|
|
63
|
+
"stats_printing_task_added",
|
|
64
|
+
interval_seconds=scheduler_config.stats_printing_interval_seconds,
|
|
65
|
+
)
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.error(
|
|
68
|
+
"stats_printing_task_add_failed",
|
|
69
|
+
error=str(e),
|
|
70
|
+
error_type=type(e).__name__,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Add pricing cache update task if enabled
|
|
74
|
+
if scheduler_config.pricing_update_enabled:
|
|
75
|
+
try:
|
|
76
|
+
# Convert hours to seconds
|
|
77
|
+
interval_seconds = scheduler_config.pricing_update_interval_hours * 3600
|
|
78
|
+
|
|
79
|
+
await scheduler.add_task(
|
|
80
|
+
task_name="pricing_cache_update",
|
|
81
|
+
task_type="pricing_cache_update",
|
|
82
|
+
interval_seconds=interval_seconds,
|
|
83
|
+
enabled=True,
|
|
84
|
+
force_refresh_on_startup=scheduler_config.pricing_force_refresh_on_startup,
|
|
85
|
+
)
|
|
86
|
+
logger.info(
|
|
87
|
+
"pricing_update_task_added",
|
|
88
|
+
interval_hours=scheduler_config.pricing_update_interval_hours,
|
|
89
|
+
force_refresh_on_startup=scheduler_config.pricing_force_refresh_on_startup,
|
|
90
|
+
)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
logger.error(
|
|
93
|
+
"pricing_update_task_add_failed",
|
|
94
|
+
error=str(e),
|
|
95
|
+
error_type=type(e).__name__,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _register_default_tasks() -> None:
|
|
100
|
+
"""Register default task types in the global registry."""
|
|
101
|
+
from .registry import get_task_registry
|
|
102
|
+
|
|
103
|
+
registry = get_task_registry()
|
|
104
|
+
|
|
105
|
+
# Only register if not already registered
|
|
106
|
+
if not registry.is_registered("pushgateway"):
|
|
107
|
+
register_task("pushgateway", PushgatewayTask)
|
|
108
|
+
if not registry.is_registered("stats_printing"):
|
|
109
|
+
register_task("stats_printing", StatsPrintingTask)
|
|
110
|
+
if not registry.is_registered("pricing_cache_update"):
|
|
111
|
+
register_task("pricing_cache_update", PricingCacheUpdateTask)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
async def start_scheduler(settings: Settings) -> Scheduler | None:
|
|
115
|
+
"""
|
|
116
|
+
Start the scheduler with configured tasks.
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
settings: Application settings
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
Scheduler instance if successful, None otherwise
|
|
123
|
+
"""
|
|
124
|
+
try:
|
|
125
|
+
if not settings.scheduler.enabled:
|
|
126
|
+
logger.info("scheduler_disabled")
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
# Register task types (only when actually starting scheduler)
|
|
130
|
+
_register_default_tasks()
|
|
131
|
+
|
|
132
|
+
# Create scheduler with settings
|
|
133
|
+
scheduler = Scheduler(
|
|
134
|
+
max_concurrent_tasks=settings.scheduler.max_concurrent_tasks,
|
|
135
|
+
graceful_shutdown_timeout=settings.scheduler.graceful_shutdown_timeout,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Start the scheduler
|
|
139
|
+
await scheduler.start()
|
|
140
|
+
|
|
141
|
+
# Setup tasks based on configuration
|
|
142
|
+
await setup_scheduler_tasks(scheduler, settings)
|
|
143
|
+
|
|
144
|
+
logger.info(
|
|
145
|
+
"scheduler_started",
|
|
146
|
+
max_concurrent_tasks=settings.scheduler.max_concurrent_tasks,
|
|
147
|
+
active_tasks=scheduler.task_count,
|
|
148
|
+
running_tasks=len(
|
|
149
|
+
[
|
|
150
|
+
name
|
|
151
|
+
for name in scheduler.list_tasks()
|
|
152
|
+
if scheduler.get_task(name).is_running
|
|
153
|
+
]
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return scheduler
|
|
158
|
+
|
|
159
|
+
except Exception as e:
|
|
160
|
+
logger.error(
|
|
161
|
+
"scheduler_start_failed",
|
|
162
|
+
error=str(e),
|
|
163
|
+
error_type=type(e).__name__,
|
|
164
|
+
)
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
async def stop_scheduler(scheduler: Scheduler | None) -> None:
|
|
169
|
+
"""
|
|
170
|
+
Stop the scheduler gracefully.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
scheduler: Scheduler instance to stop
|
|
174
|
+
"""
|
|
175
|
+
if scheduler is None:
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
try:
|
|
179
|
+
await scheduler.stop()
|
|
180
|
+
logger.info("scheduler_stopped")
|
|
181
|
+
except Exception as e:
|
|
182
|
+
logger.error(
|
|
183
|
+
"scheduler_stop_failed",
|
|
184
|
+
error=str(e),
|
|
185
|
+
error_type=type(e).__name__,
|
|
186
|
+
)
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""Task registry for dynamic task registration and discovery."""
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
import structlog
|
|
6
|
+
|
|
7
|
+
from .exceptions import TaskRegistrationError
|
|
8
|
+
from .tasks import BaseScheduledTask
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
logger = structlog.get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class TaskRegistry:
|
|
15
|
+
"""
|
|
16
|
+
Registry for managing scheduled task registration and discovery.
|
|
17
|
+
|
|
18
|
+
Provides a centralized way to register and retrieve scheduled tasks,
|
|
19
|
+
enabling dynamic task management and configuration.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
"""Initialize the task registry."""
|
|
24
|
+
self._tasks: dict[str, type[BaseScheduledTask]] = {}
|
|
25
|
+
|
|
26
|
+
def register(self, name: str, task_class: type[BaseScheduledTask]) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Register a scheduled task class.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
name: Unique name for the task
|
|
32
|
+
task_class: Task class that inherits from BaseScheduledTask
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
TaskRegistrationError: If task name is already registered or invalid
|
|
36
|
+
"""
|
|
37
|
+
if name in self._tasks:
|
|
38
|
+
raise TaskRegistrationError(f"Task '{name}' is already registered")
|
|
39
|
+
|
|
40
|
+
if not issubclass(task_class, BaseScheduledTask):
|
|
41
|
+
raise TaskRegistrationError(
|
|
42
|
+
f"Task class for '{name}' must inherit from BaseScheduledTask"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
self._tasks[name] = task_class
|
|
46
|
+
logger.debug("task_registered", task_name=name, task_class=task_class.__name__)
|
|
47
|
+
|
|
48
|
+
def unregister(self, name: str) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Unregister a scheduled task.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
name: Name of the task to unregister
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
TaskRegistrationError: If task is not registered
|
|
57
|
+
"""
|
|
58
|
+
if name not in self._tasks:
|
|
59
|
+
raise TaskRegistrationError(f"Task '{name}' is not registered")
|
|
60
|
+
|
|
61
|
+
del self._tasks[name]
|
|
62
|
+
logger.debug("task_unregistered", task_name=name)
|
|
63
|
+
|
|
64
|
+
def get(self, name: str) -> type[BaseScheduledTask]:
|
|
65
|
+
"""
|
|
66
|
+
Get a registered task class by name.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
name: Name of the task to retrieve
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Task class
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
TaskRegistrationError: If task is not registered
|
|
76
|
+
"""
|
|
77
|
+
if name not in self._tasks:
|
|
78
|
+
raise TaskRegistrationError(f"Task '{name}' is not registered")
|
|
79
|
+
|
|
80
|
+
return self._tasks[name]
|
|
81
|
+
|
|
82
|
+
def list_tasks(self) -> list[str]:
|
|
83
|
+
"""
|
|
84
|
+
Get list of all registered task names.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
List of registered task names
|
|
88
|
+
"""
|
|
89
|
+
return list(self._tasks.keys())
|
|
90
|
+
|
|
91
|
+
def is_registered(self, name: str) -> bool:
|
|
92
|
+
"""
|
|
93
|
+
Check if a task is registered.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
name: Task name to check
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
True if task is registered, False otherwise
|
|
100
|
+
"""
|
|
101
|
+
return name in self._tasks
|
|
102
|
+
|
|
103
|
+
def clear(self) -> None:
|
|
104
|
+
"""Clear all registered tasks."""
|
|
105
|
+
self._tasks.clear()
|
|
106
|
+
logger.debug("task_registry_cleared")
|
|
107
|
+
|
|
108
|
+
def get_registry_info(self) -> dict[str, Any]:
|
|
109
|
+
"""
|
|
110
|
+
Get information about the current registry state.
|
|
111
|
+
|
|
112
|
+
Returns:
|
|
113
|
+
Dictionary with registry information
|
|
114
|
+
"""
|
|
115
|
+
return {
|
|
116
|
+
"total_tasks": len(self._tasks),
|
|
117
|
+
"registered_tasks": list(self._tasks.keys()),
|
|
118
|
+
"task_classes": {name: cls.__name__ for name, cls in self._tasks.items()},
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# Global task registry instance
|
|
123
|
+
_global_registry: TaskRegistry | None = None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def get_task_registry() -> TaskRegistry:
|
|
127
|
+
"""
|
|
128
|
+
Get the global task registry instance.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Global TaskRegistry instance
|
|
132
|
+
"""
|
|
133
|
+
global _global_registry
|
|
134
|
+
|
|
135
|
+
if _global_registry is None:
|
|
136
|
+
_global_registry = TaskRegistry()
|
|
137
|
+
|
|
138
|
+
return _global_registry
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def register_task(name: str, task_class: type[BaseScheduledTask]) -> None:
|
|
142
|
+
"""
|
|
143
|
+
Register a task in the global registry.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
name: Unique name for the task
|
|
147
|
+
task_class: Task class that inherits from BaseScheduledTask
|
|
148
|
+
"""
|
|
149
|
+
registry = get_task_registry()
|
|
150
|
+
registry.register(name, task_class)
|