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.
Files changed (148) hide show
  1. ccproxy/__init__.py +4 -0
  2. ccproxy/__main__.py +7 -0
  3. ccproxy/_version.py +21 -0
  4. ccproxy/adapters/__init__.py +11 -0
  5. ccproxy/adapters/base.py +80 -0
  6. ccproxy/adapters/openai/__init__.py +43 -0
  7. ccproxy/adapters/openai/adapter.py +915 -0
  8. ccproxy/adapters/openai/models.py +412 -0
  9. ccproxy/adapters/openai/streaming.py +449 -0
  10. ccproxy/api/__init__.py +28 -0
  11. ccproxy/api/app.py +225 -0
  12. ccproxy/api/dependencies.py +140 -0
  13. ccproxy/api/middleware/__init__.py +11 -0
  14. ccproxy/api/middleware/auth.py +0 -0
  15. ccproxy/api/middleware/cors.py +55 -0
  16. ccproxy/api/middleware/errors.py +703 -0
  17. ccproxy/api/middleware/headers.py +51 -0
  18. ccproxy/api/middleware/logging.py +175 -0
  19. ccproxy/api/middleware/request_id.py +69 -0
  20. ccproxy/api/middleware/server_header.py +62 -0
  21. ccproxy/api/responses.py +84 -0
  22. ccproxy/api/routes/__init__.py +16 -0
  23. ccproxy/api/routes/claude.py +181 -0
  24. ccproxy/api/routes/health.py +489 -0
  25. ccproxy/api/routes/metrics.py +1033 -0
  26. ccproxy/api/routes/proxy.py +238 -0
  27. ccproxy/auth/__init__.py +75 -0
  28. ccproxy/auth/bearer.py +68 -0
  29. ccproxy/auth/credentials_adapter.py +93 -0
  30. ccproxy/auth/dependencies.py +229 -0
  31. ccproxy/auth/exceptions.py +79 -0
  32. ccproxy/auth/manager.py +102 -0
  33. ccproxy/auth/models.py +118 -0
  34. ccproxy/auth/oauth/__init__.py +26 -0
  35. ccproxy/auth/oauth/models.py +49 -0
  36. ccproxy/auth/oauth/routes.py +396 -0
  37. ccproxy/auth/oauth/storage.py +0 -0
  38. ccproxy/auth/storage/__init__.py +12 -0
  39. ccproxy/auth/storage/base.py +57 -0
  40. ccproxy/auth/storage/json_file.py +159 -0
  41. ccproxy/auth/storage/keyring.py +192 -0
  42. ccproxy/claude_sdk/__init__.py +20 -0
  43. ccproxy/claude_sdk/client.py +169 -0
  44. ccproxy/claude_sdk/converter.py +331 -0
  45. ccproxy/claude_sdk/options.py +120 -0
  46. ccproxy/cli/__init__.py +14 -0
  47. ccproxy/cli/commands/__init__.py +8 -0
  48. ccproxy/cli/commands/auth.py +553 -0
  49. ccproxy/cli/commands/config/__init__.py +14 -0
  50. ccproxy/cli/commands/config/commands.py +766 -0
  51. ccproxy/cli/commands/config/schema_commands.py +119 -0
  52. ccproxy/cli/commands/serve.py +630 -0
  53. ccproxy/cli/docker/__init__.py +34 -0
  54. ccproxy/cli/docker/adapter_factory.py +157 -0
  55. ccproxy/cli/docker/params.py +278 -0
  56. ccproxy/cli/helpers.py +144 -0
  57. ccproxy/cli/main.py +193 -0
  58. ccproxy/cli/options/__init__.py +14 -0
  59. ccproxy/cli/options/claude_options.py +216 -0
  60. ccproxy/cli/options/core_options.py +40 -0
  61. ccproxy/cli/options/security_options.py +48 -0
  62. ccproxy/cli/options/server_options.py +117 -0
  63. ccproxy/config/__init__.py +40 -0
  64. ccproxy/config/auth.py +154 -0
  65. ccproxy/config/claude.py +124 -0
  66. ccproxy/config/cors.py +79 -0
  67. ccproxy/config/discovery.py +87 -0
  68. ccproxy/config/docker_settings.py +265 -0
  69. ccproxy/config/loader.py +108 -0
  70. ccproxy/config/observability.py +158 -0
  71. ccproxy/config/pricing.py +88 -0
  72. ccproxy/config/reverse_proxy.py +31 -0
  73. ccproxy/config/scheduler.py +89 -0
  74. ccproxy/config/security.py +14 -0
  75. ccproxy/config/server.py +81 -0
  76. ccproxy/config/settings.py +534 -0
  77. ccproxy/config/validators.py +231 -0
  78. ccproxy/core/__init__.py +274 -0
  79. ccproxy/core/async_utils.py +675 -0
  80. ccproxy/core/constants.py +97 -0
  81. ccproxy/core/errors.py +256 -0
  82. ccproxy/core/http.py +328 -0
  83. ccproxy/core/http_transformers.py +428 -0
  84. ccproxy/core/interfaces.py +247 -0
  85. ccproxy/core/logging.py +189 -0
  86. ccproxy/core/middleware.py +114 -0
  87. ccproxy/core/proxy.py +143 -0
  88. ccproxy/core/system.py +38 -0
  89. ccproxy/core/transformers.py +259 -0
  90. ccproxy/core/types.py +129 -0
  91. ccproxy/core/validators.py +288 -0
  92. ccproxy/docker/__init__.py +67 -0
  93. ccproxy/docker/adapter.py +588 -0
  94. ccproxy/docker/docker_path.py +207 -0
  95. ccproxy/docker/middleware.py +103 -0
  96. ccproxy/docker/models.py +228 -0
  97. ccproxy/docker/protocol.py +192 -0
  98. ccproxy/docker/stream_process.py +264 -0
  99. ccproxy/docker/validators.py +173 -0
  100. ccproxy/models/__init__.py +123 -0
  101. ccproxy/models/errors.py +42 -0
  102. ccproxy/models/messages.py +243 -0
  103. ccproxy/models/requests.py +85 -0
  104. ccproxy/models/responses.py +227 -0
  105. ccproxy/models/types.py +102 -0
  106. ccproxy/observability/__init__.py +51 -0
  107. ccproxy/observability/access_logger.py +400 -0
  108. ccproxy/observability/context.py +447 -0
  109. ccproxy/observability/metrics.py +539 -0
  110. ccproxy/observability/pushgateway.py +366 -0
  111. ccproxy/observability/sse_events.py +303 -0
  112. ccproxy/observability/stats_printer.py +755 -0
  113. ccproxy/observability/storage/__init__.py +1 -0
  114. ccproxy/observability/storage/duckdb_simple.py +665 -0
  115. ccproxy/observability/storage/models.py +55 -0
  116. ccproxy/pricing/__init__.py +19 -0
  117. ccproxy/pricing/cache.py +212 -0
  118. ccproxy/pricing/loader.py +267 -0
  119. ccproxy/pricing/models.py +106 -0
  120. ccproxy/pricing/updater.py +309 -0
  121. ccproxy/scheduler/__init__.py +39 -0
  122. ccproxy/scheduler/core.py +335 -0
  123. ccproxy/scheduler/exceptions.py +34 -0
  124. ccproxy/scheduler/manager.py +186 -0
  125. ccproxy/scheduler/registry.py +150 -0
  126. ccproxy/scheduler/tasks.py +484 -0
  127. ccproxy/services/__init__.py +10 -0
  128. ccproxy/services/claude_sdk_service.py +614 -0
  129. ccproxy/services/credentials/__init__.py +55 -0
  130. ccproxy/services/credentials/config.py +105 -0
  131. ccproxy/services/credentials/manager.py +562 -0
  132. ccproxy/services/credentials/oauth_client.py +482 -0
  133. ccproxy/services/proxy_service.py +1536 -0
  134. ccproxy/static/.keep +0 -0
  135. ccproxy/testing/__init__.py +34 -0
  136. ccproxy/testing/config.py +148 -0
  137. ccproxy/testing/content_generation.py +197 -0
  138. ccproxy/testing/mock_responses.py +262 -0
  139. ccproxy/testing/response_handlers.py +161 -0
  140. ccproxy/testing/scenarios.py +241 -0
  141. ccproxy/utils/__init__.py +6 -0
  142. ccproxy/utils/cost_calculator.py +210 -0
  143. ccproxy/utils/streaming_metrics.py +199 -0
  144. ccproxy_api-0.1.0.dist-info/METADATA +253 -0
  145. ccproxy_api-0.1.0.dist-info/RECORD +148 -0
  146. ccproxy_api-0.1.0.dist-info/WHEEL +4 -0
  147. ccproxy_api-0.1.0.dist-info/entry_points.txt +2 -0
  148. 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)