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,189 @@
1
+ import logging
2
+ import sys
3
+ from collections.abc import MutableMapping
4
+ from typing import Any
5
+
6
+ import structlog
7
+ from structlog.stdlib import BoundLogger
8
+ from structlog.typing import Processor
9
+
10
+
11
+ def configure_structlog(json_logs: bool = False, log_level: str = "INFO") -> None:
12
+ """Configure structlog with your preferred processors."""
13
+ # Use different timestamp format based on log level
14
+ # Dev mode (DEBUG): only hours without microseconds
15
+ # Info mode: full date without microseconds
16
+ if log_level.upper() == "DEBUG":
17
+ timestamper = structlog.processors.TimeStamper(fmt="%H:%M:%S")
18
+ else:
19
+ timestamper = structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S")
20
+
21
+ # Processors that will be used for structlog loggers
22
+ processors: list[Processor] = [
23
+ structlog.stdlib.filter_by_level,
24
+ ]
25
+
26
+ # Only add logger name if NOT in INFO mode
27
+ if log_level.upper() != "INFO":
28
+ processors.append(structlog.stdlib.add_logger_name)
29
+
30
+ processors.extend(
31
+ [
32
+ structlog.stdlib.add_log_level,
33
+ structlog.stdlib.PositionalArgumentsFormatter(),
34
+ timestamper,
35
+ structlog.processors.StackInfoRenderer(),
36
+ structlog.processors.format_exc_info,
37
+ structlog.processors.UnicodeDecoder(),
38
+ ]
39
+ )
40
+
41
+ # Only add CallsiteParameterAdder if NOT in INFO mode
42
+ if log_level.upper() != "INFO":
43
+ processors.append(
44
+ structlog.processors.CallsiteParameterAdder(
45
+ parameters=[
46
+ structlog.processors.CallsiteParameter.FILENAME,
47
+ structlog.processors.CallsiteParameter.LINENO,
48
+ ]
49
+ )
50
+ )
51
+
52
+ # This wrapper passes the event dictionary to the ProcessorFormatter
53
+ # so we don't double-render
54
+ processors.append(structlog.stdlib.ProcessorFormatter.wrap_for_formatter)
55
+
56
+ structlog.configure(
57
+ processors=processors,
58
+ context_class=dict,
59
+ logger_factory=structlog.stdlib.LoggerFactory(),
60
+ wrapper_class=structlog.stdlib.BoundLogger,
61
+ cache_logger_on_first_use=False, # Don't cache to allow reconfiguration
62
+ )
63
+
64
+
65
+ def setup_logging(
66
+ json_logs: bool = False, log_level: str = "INFO", log_file: str | None = None
67
+ ) -> BoundLogger:
68
+ """
69
+ Setup logging for the entire application including uvicorn and fastapi.
70
+ Returns a structlog logger instance.
71
+ """
72
+ # Set the log level for the root logger first so structlog can see it
73
+ root_logger = logging.getLogger()
74
+ root_logger.setLevel(getattr(logging, log_level.upper(), logging.INFO))
75
+
76
+ # Configure structlog after setting the log level
77
+ configure_structlog(json_logs=json_logs, log_level=log_level)
78
+
79
+ # Create a handler that will format stdlib logs through structlog
80
+ handler = logging.StreamHandler(sys.stdout)
81
+ handler.setLevel(getattr(logging, log_level.upper(), logging.INFO))
82
+
83
+ # Use the appropriate renderer based on json_logs setting
84
+ renderer = (
85
+ structlog.processors.JSONRenderer()
86
+ if json_logs
87
+ else structlog.dev.ConsoleRenderer()
88
+ )
89
+
90
+ # Use ProcessorFormatter to handle both structlog and stdlib logs
91
+ # Use the same timestamp format for foreign logs
92
+ if log_level.upper() == "DEBUG":
93
+ foreign_timestamper = structlog.processors.TimeStamper(fmt="%H:%M:%S")
94
+ else:
95
+ foreign_timestamper = structlog.processors.TimeStamper(fmt="%Y-%m-%d %H:%M:%S")
96
+
97
+ # Build foreign_pre_chain conditionally
98
+ foreign_pre_chain: list[Processor] = [structlog.stdlib.add_log_level]
99
+
100
+ # Only add logger name if NOT in INFO mode
101
+ if log_level.upper() != "INFO":
102
+ foreign_pre_chain.append(structlog.stdlib.add_logger_name)
103
+
104
+ foreign_pre_chain.append(foreign_timestamper)
105
+
106
+ handler.setFormatter(
107
+ structlog.stdlib.ProcessorFormatter(
108
+ processor=renderer,
109
+ foreign_pre_chain=foreign_pre_chain,
110
+ )
111
+ )
112
+
113
+ # Configure root logger (level already set above)
114
+ handlers: list[logging.Handler] = [handler]
115
+
116
+ # Add file handler if log_file is specified
117
+ if log_file:
118
+ from pathlib import Path
119
+
120
+ # Ensure parent directory exists
121
+ log_path = Path(log_file)
122
+ log_path.parent.mkdir(parents=True, exist_ok=True)
123
+
124
+ # Create a file handler that always outputs JSON
125
+ file_handler = logging.FileHandler(log_file, encoding="utf-8")
126
+ file_handler.setLevel(getattr(logging, log_level.upper(), logging.INFO))
127
+ file_handler.setFormatter(
128
+ structlog.stdlib.ProcessorFormatter(
129
+ processor=structlog.processors.JSONRenderer(),
130
+ foreign_pre_chain=foreign_pre_chain,
131
+ )
132
+ )
133
+ handlers.append(file_handler)
134
+
135
+ root_logger.handlers = handlers
136
+
137
+ # Make sure uvicorn and fastapi loggers use our configuration
138
+ for logger_name in [
139
+ "uvicorn",
140
+ "uvicorn.access",
141
+ "uvicorn.error",
142
+ "fastapi",
143
+ "ccproxy",
144
+ ]:
145
+ logger = logging.getLogger(logger_name)
146
+ logger.handlers = [] # Remove default handlers
147
+ logger.propagate = True # Use root logger's handlers
148
+
149
+ # Set uvicorn loggers to WARNING when app log level is INFO to reduce noise
150
+ if logger_name.startswith("uvicorn") and log_level.upper() == "INFO":
151
+ logger.setLevel(logging.WARNING)
152
+ else:
153
+ logger.setLevel(getattr(logging, log_level.upper(), logging.INFO))
154
+
155
+ # Configure httpx logger separately - INFO when app is DEBUG, WARNING otherwise
156
+ httpx_logger = logging.getLogger("httpx")
157
+ httpx_logger.handlers = [] # Remove default handlers
158
+ httpx_logger.propagate = True # Use root logger's handlers
159
+ if log_level.upper() == "DEBUG":
160
+ httpx_logger.setLevel(logging.INFO)
161
+ else:
162
+ httpx_logger.setLevel(logging.WARNING)
163
+
164
+ # Set noisy HTTP-related loggers to WARNING when app log level >= WARNING, else use app log level
165
+ app_log_level = getattr(logging, log_level.upper(), logging.INFO)
166
+ noisy_log_level = (
167
+ logging.WARNING if app_log_level <= logging.WARNING else app_log_level
168
+ )
169
+
170
+ for noisy_logger_name in [
171
+ "urllib3",
172
+ "urllib3.connectionpool",
173
+ "requests",
174
+ "aiohttp",
175
+ "httpcore",
176
+ "httpcore.http11",
177
+ ]:
178
+ noisy_logger = logging.getLogger(noisy_logger_name)
179
+ noisy_logger.handlers = [] # Remove default handlers
180
+ noisy_logger.propagate = True # Use root logger's handlers
181
+ noisy_logger.setLevel(noisy_log_level)
182
+
183
+ return structlog.get_logger() # type: ignore[no-any-return]
184
+
185
+
186
+ # Create a convenience function for getting loggers
187
+ def get_logger(name: str | None = None) -> BoundLogger:
188
+ """Get a structlog logger instance."""
189
+ return structlog.get_logger(name) # type: ignore[no-any-return]
@@ -0,0 +1,114 @@
1
+ """Core middleware abstractions for request/response processing."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from collections.abc import Awaitable, Callable
5
+ from typing import Any, Optional, Protocol, runtime_checkable
6
+
7
+ from ccproxy.core.types import ProxyRequest, ProxyResponse
8
+
9
+
10
+ # Type alias for the next middleware function
11
+ NextMiddleware = Callable[[ProxyRequest], Awaitable[ProxyResponse]]
12
+
13
+
14
+ class BaseMiddleware(ABC):
15
+ """Abstract base class for all middleware."""
16
+
17
+ @abstractmethod
18
+ async def __call__(
19
+ self, request: ProxyRequest, next: NextMiddleware
20
+ ) -> ProxyResponse:
21
+ """Process the request and call the next middleware.
22
+
23
+ Args:
24
+ request: The incoming request
25
+ next: The next middleware in the chain
26
+
27
+ Returns:
28
+ The response from the middleware chain
29
+
30
+ Raises:
31
+ MiddlewareError: If middleware processing fails
32
+ """
33
+ pass
34
+
35
+
36
+ @runtime_checkable
37
+ class MiddlewareProtocol(Protocol):
38
+ """Protocol defining the middleware interface."""
39
+
40
+ async def __call__(
41
+ self, request: ProxyRequest, next: NextMiddleware
42
+ ) -> ProxyResponse:
43
+ """Process the request and call the next middleware."""
44
+ ...
45
+
46
+
47
+ class MiddlewareChain:
48
+ """Manages a chain of middleware."""
49
+
50
+ def __init__(self, middleware: list[BaseMiddleware]):
51
+ """Initialize with a list of middleware.
52
+
53
+ Args:
54
+ middleware: List of middleware to apply in order
55
+ """
56
+ self.middleware = middleware
57
+
58
+ async def __call__(
59
+ self, request: ProxyRequest, handler: NextMiddleware
60
+ ) -> ProxyResponse:
61
+ """Execute the middleware chain.
62
+
63
+ Args:
64
+ request: The incoming request
65
+ handler: The final request handler
66
+
67
+ Returns:
68
+ The response from the middleware chain
69
+ """
70
+ # Build the chain from the inside out
71
+ chain = handler
72
+ for mw in reversed(self.middleware):
73
+ # Create a closure to capture the current middleware and chain
74
+ def make_chain(
75
+ current_mw: BaseMiddleware, current_chain: NextMiddleware
76
+ ) -> NextMiddleware:
77
+ async def next_fn(req: ProxyRequest) -> ProxyResponse:
78
+ return await current_chain(req)
79
+
80
+ async def new_chain(req: ProxyRequest) -> ProxyResponse:
81
+ return await current_mw(req, next_fn)
82
+
83
+ return new_chain
84
+
85
+ chain = make_chain(mw, chain)
86
+
87
+ # Execute the complete chain
88
+ return await chain(request)
89
+
90
+
91
+ class CompositeMiddleware(BaseMiddleware):
92
+ """Middleware that combines multiple middleware into one."""
93
+
94
+ def __init__(self, middleware: list[BaseMiddleware]):
95
+ """Initialize with a list of middleware to compose.
96
+
97
+ Args:
98
+ middleware: List of middleware to apply in order
99
+ """
100
+ self.chain = MiddlewareChain(middleware)
101
+
102
+ async def __call__(
103
+ self, request: ProxyRequest, next: NextMiddleware
104
+ ) -> ProxyResponse:
105
+ """Process the request through all composed middleware.
106
+
107
+ Args:
108
+ request: The incoming request
109
+ next: The next middleware in the chain
110
+
111
+ Returns:
112
+ The response from the middleware chain
113
+ """
114
+ return await self.chain(request, next)
ccproxy/core/proxy.py ADDED
@@ -0,0 +1,143 @@
1
+ """Core proxy abstractions for handling HTTP and WebSocket connections."""
2
+
3
+ from abc import ABC, abstractmethod
4
+ from typing import TYPE_CHECKING, Any, Optional, Protocol, runtime_checkable
5
+
6
+ from ccproxy.core.types import ProxyRequest, ProxyResponse
7
+
8
+
9
+ if TYPE_CHECKING:
10
+ from ccproxy.core.http import HTTPClient
11
+
12
+
13
+ class BaseProxy(ABC):
14
+ """Abstract base class for all proxy implementations."""
15
+
16
+ @abstractmethod
17
+ async def forward(self, request: ProxyRequest) -> ProxyResponse:
18
+ """Forward a request and return the response.
19
+
20
+ Args:
21
+ request: The proxy request to forward
22
+
23
+ Returns:
24
+ The proxy response
25
+
26
+ Raises:
27
+ ProxyError: If the request cannot be forwarded
28
+ """
29
+ pass
30
+
31
+ @abstractmethod
32
+ async def close(self) -> None:
33
+ """Close any resources held by the proxy."""
34
+ pass
35
+
36
+
37
+ class HTTPProxy(BaseProxy):
38
+ """HTTP proxy implementation using HTTPClient abstractions."""
39
+
40
+ def __init__(self, http_client: "HTTPClient") -> None:
41
+ """Initialize with an HTTP client.
42
+
43
+ Args:
44
+ http_client: The HTTP client to use for requests
45
+ """
46
+ self.http_client = http_client
47
+
48
+ async def forward(self, request: ProxyRequest) -> ProxyResponse:
49
+ """Forward an HTTP request using the HTTP client.
50
+
51
+ Args:
52
+ request: The proxy request to forward
53
+
54
+ Returns:
55
+ The proxy response
56
+
57
+ Raises:
58
+ ProxyError: If the request cannot be forwarded
59
+ """
60
+ from ccproxy.core.errors import ProxyError
61
+ from ccproxy.core.http import HTTPError
62
+
63
+ try:
64
+ # Convert ProxyRequest to HTTP client format
65
+ body_bytes = None
66
+ if request.body is not None:
67
+ if isinstance(request.body, bytes):
68
+ body_bytes = request.body
69
+ elif isinstance(request.body, str):
70
+ body_bytes = request.body.encode("utf-8")
71
+ elif isinstance(request.body, dict):
72
+ import json
73
+
74
+ body_bytes = json.dumps(request.body).encode("utf-8")
75
+
76
+ # Make the HTTP request
77
+ status_code, headers, response_body = await self.http_client.request(
78
+ method=request.method.value,
79
+ url=request.url,
80
+ headers=request.headers,
81
+ body=body_bytes,
82
+ timeout=request.timeout,
83
+ )
84
+
85
+ # Convert response body to appropriate format
86
+ body: str | bytes | dict[str, Any] | None = response_body
87
+ if response_body:
88
+ # Try to decode as JSON if content-type suggests it
89
+ content_type = headers.get("content-type", "").lower()
90
+ if "application/json" in content_type:
91
+ try:
92
+ import json
93
+
94
+ body = json.loads(response_body.decode("utf-8"))
95
+ except (json.JSONDecodeError, UnicodeDecodeError):
96
+ # Keep as bytes if JSON parsing fails
97
+ body = response_body
98
+ elif "text/" in content_type:
99
+ try:
100
+ body = response_body.decode("utf-8")
101
+ except UnicodeDecodeError:
102
+ # Keep as bytes if text decoding fails
103
+ body = response_body
104
+
105
+ return ProxyResponse(
106
+ status_code=status_code,
107
+ headers=headers,
108
+ body=body,
109
+ )
110
+
111
+ except HTTPError as e:
112
+ raise ProxyError(f"HTTP request failed: {e}") from e
113
+ except Exception as e:
114
+ raise ProxyError(f"Unexpected error during HTTP request: {e}") from e
115
+
116
+ async def close(self) -> None:
117
+ """Close HTTP proxy resources."""
118
+ await self.http_client.close()
119
+
120
+
121
+ class WebSocketProxy(BaseProxy):
122
+ """WebSocket proxy implementation placeholder."""
123
+
124
+ async def forward(self, request: ProxyRequest) -> ProxyResponse:
125
+ """Forward a WebSocket request."""
126
+ raise NotImplementedError("WebSocketProxy.forward not yet implemented")
127
+
128
+ async def close(self) -> None:
129
+ """Close WebSocket proxy resources."""
130
+ pass
131
+
132
+
133
+ @runtime_checkable
134
+ class ProxyProtocol(Protocol):
135
+ """Protocol defining the proxy interface."""
136
+
137
+ async def forward(self, request: ProxyRequest) -> ProxyResponse:
138
+ """Forward a request and return the response."""
139
+ ...
140
+
141
+ async def close(self) -> None:
142
+ """Close any resources held by the proxy."""
143
+ ...
ccproxy/core/system.py ADDED
@@ -0,0 +1,38 @@
1
+ import os
2
+ from pathlib import Path
3
+
4
+
5
+ def get_xdg_config_home() -> Path:
6
+ """Get the XDG_CONFIG_HOME directory.
7
+
8
+ Returns:
9
+ Path to the XDG config directory. Falls back to ~/.config if not set.
10
+ """
11
+ xdg_config_home = os.environ.get("XDG_CONFIG_HOME")
12
+ if xdg_config_home:
13
+ return Path(xdg_config_home)
14
+ return Path.home() / ".config"
15
+
16
+
17
+ def get_xdg_data_home() -> Path:
18
+ """Get the XDG_DATA_HOME directory.
19
+
20
+ Returns:
21
+ Path to the XDG data directory. Falls back to ~/.local/share if not set.
22
+ """
23
+ xdg_data_home = os.environ.get("XDG_DATA_HOME")
24
+ if xdg_data_home:
25
+ return Path(xdg_data_home)
26
+ return Path.home() / ".local" / "share"
27
+
28
+
29
+ def get_xdg_cache_home() -> Path:
30
+ """Get the XDG_CACHE_HOME directory.
31
+
32
+ Returns:
33
+ Path to the XDG cache directory. Falls back to ~/.cache if not set.
34
+ """
35
+ xdg_cache_home = os.environ.get("XDG_CACHE_HOME")
36
+ if xdg_cache_home:
37
+ return Path(xdg_cache_home)
38
+ return Path.home() / ".cache"