ccproxy-api 0.1.6__py3-none-any.whl → 0.2.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 (481) hide show
  1. ccproxy/api/__init__.py +1 -15
  2. ccproxy/api/app.py +439 -212
  3. ccproxy/api/bootstrap.py +30 -0
  4. ccproxy/api/decorators.py +85 -0
  5. ccproxy/api/dependencies.py +145 -176
  6. ccproxy/api/format_validation.py +54 -0
  7. ccproxy/api/middleware/cors.py +6 -3
  8. ccproxy/api/middleware/errors.py +402 -530
  9. ccproxy/api/middleware/hooks.py +563 -0
  10. ccproxy/api/middleware/normalize_headers.py +59 -0
  11. ccproxy/api/middleware/request_id.py +35 -16
  12. ccproxy/api/middleware/streaming_hooks.py +292 -0
  13. ccproxy/api/routes/__init__.py +5 -14
  14. ccproxy/api/routes/health.py +39 -672
  15. ccproxy/api/routes/plugins.py +277 -0
  16. ccproxy/auth/__init__.py +2 -19
  17. ccproxy/auth/bearer.py +25 -15
  18. ccproxy/auth/dependencies.py +123 -157
  19. ccproxy/auth/exceptions.py +0 -12
  20. ccproxy/auth/manager.py +35 -49
  21. ccproxy/auth/managers/__init__.py +10 -0
  22. ccproxy/auth/managers/base.py +523 -0
  23. ccproxy/auth/managers/base_enhanced.py +63 -0
  24. ccproxy/auth/managers/token_snapshot.py +77 -0
  25. ccproxy/auth/models/base.py +65 -0
  26. ccproxy/auth/models/credentials.py +40 -0
  27. ccproxy/auth/oauth/__init__.py +4 -18
  28. ccproxy/auth/oauth/base.py +533 -0
  29. ccproxy/auth/oauth/cli_errors.py +37 -0
  30. ccproxy/auth/oauth/flows.py +430 -0
  31. ccproxy/auth/oauth/protocol.py +366 -0
  32. ccproxy/auth/oauth/registry.py +408 -0
  33. ccproxy/auth/oauth/router.py +396 -0
  34. ccproxy/auth/oauth/routes.py +186 -113
  35. ccproxy/auth/oauth/session.py +151 -0
  36. ccproxy/auth/oauth/templates.py +342 -0
  37. ccproxy/auth/storage/__init__.py +2 -5
  38. ccproxy/auth/storage/base.py +279 -5
  39. ccproxy/auth/storage/generic.py +134 -0
  40. ccproxy/cli/__init__.py +1 -2
  41. ccproxy/cli/_settings_help.py +351 -0
  42. ccproxy/cli/commands/auth.py +1519 -793
  43. ccproxy/cli/commands/config/commands.py +209 -276
  44. ccproxy/cli/commands/plugins.py +669 -0
  45. ccproxy/cli/commands/serve.py +75 -810
  46. ccproxy/cli/commands/status.py +254 -0
  47. ccproxy/cli/decorators.py +83 -0
  48. ccproxy/cli/helpers.py +22 -60
  49. ccproxy/cli/main.py +359 -10
  50. ccproxy/cli/options/claude_options.py +0 -25
  51. ccproxy/config/__init__.py +7 -11
  52. ccproxy/config/core.py +227 -0
  53. ccproxy/config/env_generator.py +232 -0
  54. ccproxy/config/runtime.py +67 -0
  55. ccproxy/config/security.py +36 -3
  56. ccproxy/config/settings.py +382 -441
  57. ccproxy/config/toml_generator.py +299 -0
  58. ccproxy/config/utils.py +452 -0
  59. ccproxy/core/__init__.py +7 -271
  60. ccproxy/{_version.py → core/_version.py} +16 -3
  61. ccproxy/core/async_task_manager.py +516 -0
  62. ccproxy/core/async_utils.py +47 -14
  63. ccproxy/core/auth/__init__.py +6 -0
  64. ccproxy/core/constants.py +16 -50
  65. ccproxy/core/errors.py +53 -0
  66. ccproxy/core/id_utils.py +20 -0
  67. ccproxy/core/interfaces.py +16 -123
  68. ccproxy/core/logging.py +473 -18
  69. ccproxy/core/plugins/__init__.py +77 -0
  70. ccproxy/core/plugins/cli_discovery.py +211 -0
  71. ccproxy/core/plugins/declaration.py +455 -0
  72. ccproxy/core/plugins/discovery.py +604 -0
  73. ccproxy/core/plugins/factories.py +967 -0
  74. ccproxy/core/plugins/hooks/__init__.py +30 -0
  75. ccproxy/core/plugins/hooks/base.py +58 -0
  76. ccproxy/core/plugins/hooks/events.py +46 -0
  77. ccproxy/core/plugins/hooks/implementations/__init__.py +16 -0
  78. ccproxy/core/plugins/hooks/implementations/formatters/__init__.py +11 -0
  79. ccproxy/core/plugins/hooks/implementations/formatters/json.py +552 -0
  80. ccproxy/core/plugins/hooks/implementations/formatters/raw.py +370 -0
  81. ccproxy/core/plugins/hooks/implementations/http_tracer.py +431 -0
  82. ccproxy/core/plugins/hooks/layers.py +44 -0
  83. ccproxy/core/plugins/hooks/manager.py +186 -0
  84. ccproxy/core/plugins/hooks/registry.py +139 -0
  85. ccproxy/core/plugins/hooks/thread_manager.py +203 -0
  86. ccproxy/core/plugins/hooks/types.py +22 -0
  87. ccproxy/core/plugins/interfaces.py +416 -0
  88. ccproxy/core/plugins/loader.py +166 -0
  89. ccproxy/core/plugins/middleware.py +233 -0
  90. ccproxy/core/plugins/models.py +59 -0
  91. ccproxy/core/plugins/protocol.py +180 -0
  92. ccproxy/core/plugins/runtime.py +519 -0
  93. ccproxy/{observability/context.py → core/request_context.py} +137 -94
  94. ccproxy/core/status_report.py +211 -0
  95. ccproxy/core/transformers.py +13 -8
  96. ccproxy/data/claude_headers_fallback.json +558 -0
  97. ccproxy/data/codex_headers_fallback.json +121 -0
  98. ccproxy/http/__init__.py +30 -0
  99. ccproxy/http/base.py +95 -0
  100. ccproxy/http/client.py +323 -0
  101. ccproxy/http/hooks.py +642 -0
  102. ccproxy/http/pool.py +279 -0
  103. ccproxy/llms/formatters/__init__.py +7 -0
  104. ccproxy/llms/formatters/anthropic_to_openai/__init__.py +55 -0
  105. ccproxy/llms/formatters/anthropic_to_openai/errors.py +65 -0
  106. ccproxy/llms/formatters/anthropic_to_openai/requests.py +356 -0
  107. ccproxy/llms/formatters/anthropic_to_openai/responses.py +153 -0
  108. ccproxy/llms/formatters/anthropic_to_openai/streams.py +1546 -0
  109. ccproxy/llms/formatters/base.py +140 -0
  110. ccproxy/llms/formatters/base_model.py +33 -0
  111. ccproxy/llms/formatters/common/__init__.py +51 -0
  112. ccproxy/llms/formatters/common/identifiers.py +48 -0
  113. ccproxy/llms/formatters/common/streams.py +254 -0
  114. ccproxy/llms/formatters/common/thinking.py +74 -0
  115. ccproxy/llms/formatters/common/usage.py +135 -0
  116. ccproxy/llms/formatters/constants.py +55 -0
  117. ccproxy/llms/formatters/context.py +116 -0
  118. ccproxy/llms/formatters/mapping.py +33 -0
  119. ccproxy/llms/formatters/openai_to_anthropic/__init__.py +55 -0
  120. ccproxy/llms/formatters/openai_to_anthropic/_helpers.py +141 -0
  121. ccproxy/llms/formatters/openai_to_anthropic/errors.py +53 -0
  122. ccproxy/llms/formatters/openai_to_anthropic/requests.py +674 -0
  123. ccproxy/llms/formatters/openai_to_anthropic/responses.py +285 -0
  124. ccproxy/llms/formatters/openai_to_anthropic/streams.py +530 -0
  125. ccproxy/llms/formatters/openai_to_openai/__init__.py +53 -0
  126. ccproxy/llms/formatters/openai_to_openai/_helpers.py +325 -0
  127. ccproxy/llms/formatters/openai_to_openai/errors.py +6 -0
  128. ccproxy/llms/formatters/openai_to_openai/requests.py +388 -0
  129. ccproxy/llms/formatters/openai_to_openai/responses.py +594 -0
  130. ccproxy/llms/formatters/openai_to_openai/streams.py +1832 -0
  131. ccproxy/llms/formatters/utils.py +306 -0
  132. ccproxy/llms/models/__init__.py +9 -0
  133. ccproxy/llms/models/anthropic.py +619 -0
  134. ccproxy/llms/models/openai.py +844 -0
  135. ccproxy/llms/streaming/__init__.py +26 -0
  136. ccproxy/llms/streaming/accumulators.py +1074 -0
  137. ccproxy/llms/streaming/formatters.py +251 -0
  138. ccproxy/{adapters/openai/streaming.py → llms/streaming/processors.py} +193 -240
  139. ccproxy/models/__init__.py +8 -159
  140. ccproxy/models/detection.py +92 -193
  141. ccproxy/models/provider.py +75 -0
  142. ccproxy/plugins/access_log/README.md +32 -0
  143. ccproxy/plugins/access_log/__init__.py +20 -0
  144. ccproxy/plugins/access_log/config.py +33 -0
  145. ccproxy/plugins/access_log/formatter.py +126 -0
  146. ccproxy/plugins/access_log/hook.py +763 -0
  147. ccproxy/plugins/access_log/logger.py +254 -0
  148. ccproxy/plugins/access_log/plugin.py +137 -0
  149. ccproxy/plugins/access_log/writer.py +109 -0
  150. ccproxy/plugins/analytics/README.md +24 -0
  151. ccproxy/plugins/analytics/__init__.py +1 -0
  152. ccproxy/plugins/analytics/config.py +5 -0
  153. ccproxy/plugins/analytics/ingest.py +85 -0
  154. ccproxy/plugins/analytics/models.py +97 -0
  155. ccproxy/plugins/analytics/plugin.py +121 -0
  156. ccproxy/plugins/analytics/routes.py +163 -0
  157. ccproxy/plugins/analytics/service.py +284 -0
  158. ccproxy/plugins/claude_api/README.md +29 -0
  159. ccproxy/plugins/claude_api/__init__.py +10 -0
  160. ccproxy/plugins/claude_api/adapter.py +829 -0
  161. ccproxy/plugins/claude_api/config.py +52 -0
  162. ccproxy/plugins/claude_api/detection_service.py +461 -0
  163. ccproxy/plugins/claude_api/health.py +175 -0
  164. ccproxy/plugins/claude_api/hooks.py +284 -0
  165. ccproxy/plugins/claude_api/models.py +256 -0
  166. ccproxy/plugins/claude_api/plugin.py +298 -0
  167. ccproxy/plugins/claude_api/routes.py +118 -0
  168. ccproxy/plugins/claude_api/streaming_metrics.py +68 -0
  169. ccproxy/plugins/claude_api/tasks.py +84 -0
  170. ccproxy/plugins/claude_sdk/README.md +35 -0
  171. ccproxy/plugins/claude_sdk/__init__.py +80 -0
  172. ccproxy/plugins/claude_sdk/adapter.py +749 -0
  173. ccproxy/plugins/claude_sdk/auth.py +57 -0
  174. ccproxy/{claude_sdk → plugins/claude_sdk}/client.py +63 -39
  175. ccproxy/plugins/claude_sdk/config.py +210 -0
  176. ccproxy/{claude_sdk → plugins/claude_sdk}/converter.py +6 -6
  177. ccproxy/plugins/claude_sdk/detection_service.py +163 -0
  178. ccproxy/{services/claude_sdk_service.py → plugins/claude_sdk/handler.py} +123 -304
  179. ccproxy/plugins/claude_sdk/health.py +113 -0
  180. ccproxy/plugins/claude_sdk/hooks.py +115 -0
  181. ccproxy/{claude_sdk → plugins/claude_sdk}/manager.py +42 -32
  182. ccproxy/{claude_sdk → plugins/claude_sdk}/message_queue.py +8 -8
  183. ccproxy/{models/claude_sdk.py → plugins/claude_sdk/models.py} +64 -16
  184. ccproxy/plugins/claude_sdk/options.py +154 -0
  185. ccproxy/{claude_sdk → plugins/claude_sdk}/parser.py +23 -5
  186. ccproxy/plugins/claude_sdk/plugin.py +269 -0
  187. ccproxy/plugins/claude_sdk/routes.py +104 -0
  188. ccproxy/{claude_sdk → plugins/claude_sdk}/session_client.py +124 -12
  189. ccproxy/plugins/claude_sdk/session_pool.py +700 -0
  190. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_handle.py +48 -43
  191. ccproxy/{claude_sdk → plugins/claude_sdk}/stream_worker.py +22 -18
  192. ccproxy/{claude_sdk → plugins/claude_sdk}/streaming.py +50 -16
  193. ccproxy/plugins/claude_sdk/tasks.py +97 -0
  194. ccproxy/plugins/claude_shared/README.md +18 -0
  195. ccproxy/plugins/claude_shared/__init__.py +12 -0
  196. ccproxy/plugins/claude_shared/model_defaults.py +171 -0
  197. ccproxy/plugins/codex/README.md +35 -0
  198. ccproxy/plugins/codex/__init__.py +6 -0
  199. ccproxy/plugins/codex/adapter.py +635 -0
  200. ccproxy/{config/codex.py → plugins/codex/config.py} +78 -12
  201. ccproxy/plugins/codex/detection_service.py +544 -0
  202. ccproxy/plugins/codex/health.py +162 -0
  203. ccproxy/plugins/codex/hooks.py +263 -0
  204. ccproxy/plugins/codex/model_defaults.py +39 -0
  205. ccproxy/plugins/codex/models.py +263 -0
  206. ccproxy/plugins/codex/plugin.py +275 -0
  207. ccproxy/plugins/codex/routes.py +129 -0
  208. ccproxy/plugins/codex/streaming_metrics.py +324 -0
  209. ccproxy/plugins/codex/tasks.py +106 -0
  210. ccproxy/plugins/codex/utils/__init__.py +1 -0
  211. ccproxy/plugins/codex/utils/sse_parser.py +106 -0
  212. ccproxy/plugins/command_replay/README.md +34 -0
  213. ccproxy/plugins/command_replay/__init__.py +17 -0
  214. ccproxy/plugins/command_replay/config.py +133 -0
  215. ccproxy/plugins/command_replay/formatter.py +432 -0
  216. ccproxy/plugins/command_replay/hook.py +294 -0
  217. ccproxy/plugins/command_replay/plugin.py +161 -0
  218. ccproxy/plugins/copilot/README.md +39 -0
  219. ccproxy/plugins/copilot/__init__.py +11 -0
  220. ccproxy/plugins/copilot/adapter.py +465 -0
  221. ccproxy/plugins/copilot/config.py +155 -0
  222. ccproxy/plugins/copilot/data/copilot_fallback.json +41 -0
  223. ccproxy/plugins/copilot/detection_service.py +255 -0
  224. ccproxy/plugins/copilot/manager.py +275 -0
  225. ccproxy/plugins/copilot/model_defaults.py +284 -0
  226. ccproxy/plugins/copilot/models.py +148 -0
  227. ccproxy/plugins/copilot/oauth/__init__.py +16 -0
  228. ccproxy/plugins/copilot/oauth/client.py +494 -0
  229. ccproxy/plugins/copilot/oauth/models.py +385 -0
  230. ccproxy/plugins/copilot/oauth/provider.py +602 -0
  231. ccproxy/plugins/copilot/oauth/storage.py +170 -0
  232. ccproxy/plugins/copilot/plugin.py +360 -0
  233. ccproxy/plugins/copilot/routes.py +294 -0
  234. ccproxy/plugins/credential_balancer/README.md +124 -0
  235. ccproxy/plugins/credential_balancer/__init__.py +6 -0
  236. ccproxy/plugins/credential_balancer/config.py +270 -0
  237. ccproxy/plugins/credential_balancer/factory.py +415 -0
  238. ccproxy/plugins/credential_balancer/hook.py +51 -0
  239. ccproxy/plugins/credential_balancer/manager.py +587 -0
  240. ccproxy/plugins/credential_balancer/plugin.py +146 -0
  241. ccproxy/plugins/dashboard/README.md +25 -0
  242. ccproxy/plugins/dashboard/__init__.py +1 -0
  243. ccproxy/plugins/dashboard/config.py +8 -0
  244. ccproxy/plugins/dashboard/plugin.py +71 -0
  245. ccproxy/plugins/dashboard/routes.py +67 -0
  246. ccproxy/plugins/docker/README.md +32 -0
  247. ccproxy/{docker → plugins/docker}/__init__.py +3 -0
  248. ccproxy/{docker → plugins/docker}/adapter.py +108 -10
  249. ccproxy/plugins/docker/config.py +82 -0
  250. ccproxy/{docker → plugins/docker}/docker_path.py +4 -3
  251. ccproxy/{docker → plugins/docker}/middleware.py +2 -2
  252. ccproxy/plugins/docker/plugin.py +198 -0
  253. ccproxy/{docker → plugins/docker}/stream_process.py +3 -3
  254. ccproxy/plugins/duckdb_storage/README.md +26 -0
  255. ccproxy/plugins/duckdb_storage/__init__.py +1 -0
  256. ccproxy/plugins/duckdb_storage/config.py +22 -0
  257. ccproxy/plugins/duckdb_storage/plugin.py +128 -0
  258. ccproxy/plugins/duckdb_storage/routes.py +51 -0
  259. ccproxy/plugins/duckdb_storage/storage.py +633 -0
  260. ccproxy/plugins/max_tokens/README.md +38 -0
  261. ccproxy/plugins/max_tokens/__init__.py +12 -0
  262. ccproxy/plugins/max_tokens/adapter.py +235 -0
  263. ccproxy/plugins/max_tokens/config.py +86 -0
  264. ccproxy/plugins/max_tokens/models.py +53 -0
  265. ccproxy/plugins/max_tokens/plugin.py +200 -0
  266. ccproxy/plugins/max_tokens/service.py +271 -0
  267. ccproxy/plugins/max_tokens/token_limits.json +54 -0
  268. ccproxy/plugins/metrics/README.md +35 -0
  269. ccproxy/plugins/metrics/__init__.py +10 -0
  270. ccproxy/{observability/metrics.py → plugins/metrics/collector.py} +20 -153
  271. ccproxy/plugins/metrics/config.py +85 -0
  272. ccproxy/plugins/metrics/grafana/dashboards/ccproxy-dashboard.json +1720 -0
  273. ccproxy/plugins/metrics/hook.py +403 -0
  274. ccproxy/plugins/metrics/plugin.py +268 -0
  275. ccproxy/{observability → plugins/metrics}/pushgateway.py +57 -59
  276. ccproxy/plugins/metrics/routes.py +107 -0
  277. ccproxy/plugins/metrics/tasks.py +117 -0
  278. ccproxy/plugins/oauth_claude/README.md +35 -0
  279. ccproxy/plugins/oauth_claude/__init__.py +14 -0
  280. ccproxy/plugins/oauth_claude/client.py +270 -0
  281. ccproxy/plugins/oauth_claude/config.py +84 -0
  282. ccproxy/plugins/oauth_claude/manager.py +482 -0
  283. ccproxy/plugins/oauth_claude/models.py +266 -0
  284. ccproxy/plugins/oauth_claude/plugin.py +149 -0
  285. ccproxy/plugins/oauth_claude/provider.py +571 -0
  286. ccproxy/plugins/oauth_claude/storage.py +212 -0
  287. ccproxy/plugins/oauth_codex/README.md +38 -0
  288. ccproxy/plugins/oauth_codex/__init__.py +14 -0
  289. ccproxy/plugins/oauth_codex/client.py +224 -0
  290. ccproxy/plugins/oauth_codex/config.py +95 -0
  291. ccproxy/plugins/oauth_codex/manager.py +256 -0
  292. ccproxy/plugins/oauth_codex/models.py +239 -0
  293. ccproxy/plugins/oauth_codex/plugin.py +146 -0
  294. ccproxy/plugins/oauth_codex/provider.py +574 -0
  295. ccproxy/plugins/oauth_codex/storage.py +92 -0
  296. ccproxy/plugins/permissions/README.md +28 -0
  297. ccproxy/plugins/permissions/__init__.py +22 -0
  298. ccproxy/plugins/permissions/config.py +28 -0
  299. ccproxy/{cli/commands/permission_handler.py → plugins/permissions/handlers/cli.py} +49 -25
  300. ccproxy/plugins/permissions/handlers/protocol.py +33 -0
  301. ccproxy/plugins/permissions/handlers/terminal.py +675 -0
  302. ccproxy/{api/routes → plugins/permissions}/mcp.py +34 -7
  303. ccproxy/{models/permissions.py → plugins/permissions/models.py} +65 -1
  304. ccproxy/plugins/permissions/plugin.py +153 -0
  305. ccproxy/{api/routes/permissions.py → plugins/permissions/routes.py} +20 -16
  306. ccproxy/{api/services/permission_service.py → plugins/permissions/service.py} +65 -11
  307. ccproxy/{api → plugins/permissions}/ui/permission_handler_protocol.py +1 -1
  308. ccproxy/{api → plugins/permissions}/ui/terminal_permission_handler.py +66 -10
  309. ccproxy/plugins/pricing/README.md +34 -0
  310. ccproxy/plugins/pricing/__init__.py +6 -0
  311. ccproxy/{pricing → plugins/pricing}/cache.py +7 -6
  312. ccproxy/{config/pricing.py → plugins/pricing/config.py} +32 -6
  313. ccproxy/plugins/pricing/exceptions.py +35 -0
  314. ccproxy/plugins/pricing/loader.py +440 -0
  315. ccproxy/{pricing → plugins/pricing}/models.py +13 -23
  316. ccproxy/plugins/pricing/plugin.py +169 -0
  317. ccproxy/plugins/pricing/service.py +191 -0
  318. ccproxy/plugins/pricing/tasks.py +300 -0
  319. ccproxy/{pricing → plugins/pricing}/updater.py +86 -72
  320. ccproxy/plugins/pricing/utils.py +99 -0
  321. ccproxy/plugins/request_tracer/README.md +40 -0
  322. ccproxy/plugins/request_tracer/__init__.py +7 -0
  323. ccproxy/plugins/request_tracer/config.py +120 -0
  324. ccproxy/plugins/request_tracer/hook.py +415 -0
  325. ccproxy/plugins/request_tracer/plugin.py +255 -0
  326. ccproxy/scheduler/__init__.py +2 -14
  327. ccproxy/scheduler/core.py +26 -41
  328. ccproxy/scheduler/manager.py +63 -107
  329. ccproxy/scheduler/registry.py +6 -32
  330. ccproxy/scheduler/tasks.py +346 -314
  331. ccproxy/services/__init__.py +0 -1
  332. ccproxy/services/adapters/__init__.py +11 -0
  333. ccproxy/services/adapters/base.py +123 -0
  334. ccproxy/services/adapters/chain_composer.py +88 -0
  335. ccproxy/services/adapters/chain_validation.py +44 -0
  336. ccproxy/services/adapters/chat_accumulator.py +200 -0
  337. ccproxy/services/adapters/delta_utils.py +142 -0
  338. ccproxy/services/adapters/format_adapter.py +136 -0
  339. ccproxy/services/adapters/format_context.py +11 -0
  340. ccproxy/services/adapters/format_registry.py +158 -0
  341. ccproxy/services/adapters/http_adapter.py +1045 -0
  342. ccproxy/services/adapters/mock_adapter.py +118 -0
  343. ccproxy/services/adapters/protocols.py +35 -0
  344. ccproxy/services/adapters/simple_converters.py +571 -0
  345. ccproxy/services/auth_registry.py +180 -0
  346. ccproxy/services/cache/__init__.py +6 -0
  347. ccproxy/services/cache/response_cache.py +261 -0
  348. ccproxy/services/cli_detection.py +437 -0
  349. ccproxy/services/config/__init__.py +6 -0
  350. ccproxy/services/config/proxy_configuration.py +111 -0
  351. ccproxy/services/container.py +256 -0
  352. ccproxy/services/factories.py +380 -0
  353. ccproxy/services/handler_config.py +76 -0
  354. ccproxy/services/interfaces.py +298 -0
  355. ccproxy/services/mocking/__init__.py +6 -0
  356. ccproxy/services/mocking/mock_handler.py +291 -0
  357. ccproxy/services/tracing/__init__.py +7 -0
  358. ccproxy/services/tracing/interfaces.py +61 -0
  359. ccproxy/services/tracing/null_tracer.py +57 -0
  360. ccproxy/streaming/__init__.py +23 -0
  361. ccproxy/streaming/buffer.py +1056 -0
  362. ccproxy/streaming/deferred.py +897 -0
  363. ccproxy/streaming/handler.py +117 -0
  364. ccproxy/streaming/interfaces.py +77 -0
  365. ccproxy/streaming/simple_adapter.py +39 -0
  366. ccproxy/streaming/sse.py +109 -0
  367. ccproxy/streaming/sse_parser.py +127 -0
  368. ccproxy/templates/__init__.py +6 -0
  369. ccproxy/templates/plugin_scaffold.py +695 -0
  370. ccproxy/testing/endpoints/__init__.py +33 -0
  371. ccproxy/testing/endpoints/cli.py +215 -0
  372. ccproxy/testing/endpoints/config.py +874 -0
  373. ccproxy/testing/endpoints/console.py +57 -0
  374. ccproxy/testing/endpoints/models.py +100 -0
  375. ccproxy/testing/endpoints/runner.py +1903 -0
  376. ccproxy/testing/endpoints/tools.py +308 -0
  377. ccproxy/testing/mock_responses.py +70 -1
  378. ccproxy/testing/response_handlers.py +20 -0
  379. ccproxy/utils/__init__.py +0 -6
  380. ccproxy/utils/binary_resolver.py +476 -0
  381. ccproxy/utils/caching.py +327 -0
  382. ccproxy/utils/cli_logging.py +101 -0
  383. ccproxy/utils/command_line.py +251 -0
  384. ccproxy/utils/headers.py +228 -0
  385. ccproxy/utils/model_mapper.py +120 -0
  386. ccproxy/utils/startup_helpers.py +95 -342
  387. ccproxy/utils/version_checker.py +279 -6
  388. ccproxy_api-0.2.0.dist-info/METADATA +212 -0
  389. ccproxy_api-0.2.0.dist-info/RECORD +417 -0
  390. {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/WHEEL +1 -1
  391. ccproxy_api-0.2.0.dist-info/entry_points.txt +24 -0
  392. ccproxy/__init__.py +0 -4
  393. ccproxy/adapters/__init__.py +0 -11
  394. ccproxy/adapters/base.py +0 -80
  395. ccproxy/adapters/codex/__init__.py +0 -11
  396. ccproxy/adapters/openai/__init__.py +0 -42
  397. ccproxy/adapters/openai/adapter.py +0 -953
  398. ccproxy/adapters/openai/models.py +0 -412
  399. ccproxy/adapters/openai/response_adapter.py +0 -355
  400. ccproxy/adapters/openai/response_models.py +0 -178
  401. ccproxy/api/middleware/headers.py +0 -49
  402. ccproxy/api/middleware/logging.py +0 -180
  403. ccproxy/api/middleware/request_content_logging.py +0 -297
  404. ccproxy/api/middleware/server_header.py +0 -58
  405. ccproxy/api/responses.py +0 -89
  406. ccproxy/api/routes/claude.py +0 -371
  407. ccproxy/api/routes/codex.py +0 -1231
  408. ccproxy/api/routes/metrics.py +0 -1029
  409. ccproxy/api/routes/proxy.py +0 -211
  410. ccproxy/api/services/__init__.py +0 -6
  411. ccproxy/auth/conditional.py +0 -84
  412. ccproxy/auth/credentials_adapter.py +0 -93
  413. ccproxy/auth/models.py +0 -118
  414. ccproxy/auth/oauth/models.py +0 -48
  415. ccproxy/auth/openai/__init__.py +0 -13
  416. ccproxy/auth/openai/credentials.py +0 -166
  417. ccproxy/auth/openai/oauth_client.py +0 -334
  418. ccproxy/auth/openai/storage.py +0 -184
  419. ccproxy/auth/storage/json_file.py +0 -158
  420. ccproxy/auth/storage/keyring.py +0 -189
  421. ccproxy/claude_sdk/__init__.py +0 -18
  422. ccproxy/claude_sdk/options.py +0 -194
  423. ccproxy/claude_sdk/session_pool.py +0 -550
  424. ccproxy/cli/docker/__init__.py +0 -34
  425. ccproxy/cli/docker/adapter_factory.py +0 -157
  426. ccproxy/cli/docker/params.py +0 -274
  427. ccproxy/config/auth.py +0 -153
  428. ccproxy/config/claude.py +0 -348
  429. ccproxy/config/cors.py +0 -79
  430. ccproxy/config/discovery.py +0 -95
  431. ccproxy/config/docker_settings.py +0 -264
  432. ccproxy/config/observability.py +0 -158
  433. ccproxy/config/reverse_proxy.py +0 -31
  434. ccproxy/config/scheduler.py +0 -108
  435. ccproxy/config/server.py +0 -86
  436. ccproxy/config/validators.py +0 -231
  437. ccproxy/core/codex_transformers.py +0 -389
  438. ccproxy/core/http.py +0 -328
  439. ccproxy/core/http_transformers.py +0 -812
  440. ccproxy/core/proxy.py +0 -143
  441. ccproxy/core/validators.py +0 -288
  442. ccproxy/models/errors.py +0 -42
  443. ccproxy/models/messages.py +0 -269
  444. ccproxy/models/requests.py +0 -107
  445. ccproxy/models/responses.py +0 -270
  446. ccproxy/models/types.py +0 -102
  447. ccproxy/observability/__init__.py +0 -51
  448. ccproxy/observability/access_logger.py +0 -457
  449. ccproxy/observability/sse_events.py +0 -303
  450. ccproxy/observability/stats_printer.py +0 -753
  451. ccproxy/observability/storage/__init__.py +0 -1
  452. ccproxy/observability/storage/duckdb_simple.py +0 -677
  453. ccproxy/observability/storage/models.py +0 -70
  454. ccproxy/observability/streaming_response.py +0 -107
  455. ccproxy/pricing/__init__.py +0 -19
  456. ccproxy/pricing/loader.py +0 -251
  457. ccproxy/services/claude_detection_service.py +0 -269
  458. ccproxy/services/codex_detection_service.py +0 -263
  459. ccproxy/services/credentials/__init__.py +0 -55
  460. ccproxy/services/credentials/config.py +0 -105
  461. ccproxy/services/credentials/manager.py +0 -561
  462. ccproxy/services/credentials/oauth_client.py +0 -481
  463. ccproxy/services/proxy_service.py +0 -1827
  464. ccproxy/static/.keep +0 -0
  465. ccproxy/utils/cost_calculator.py +0 -210
  466. ccproxy/utils/disconnection_monitor.py +0 -83
  467. ccproxy/utils/model_mapping.py +0 -199
  468. ccproxy/utils/models_provider.py +0 -150
  469. ccproxy/utils/simple_request_logger.py +0 -284
  470. ccproxy/utils/streaming_metrics.py +0 -199
  471. ccproxy_api-0.1.6.dist-info/METADATA +0 -615
  472. ccproxy_api-0.1.6.dist-info/RECORD +0 -189
  473. ccproxy_api-0.1.6.dist-info/entry_points.txt +0 -4
  474. /ccproxy/{api/middleware/auth.py → auth/models/__init__.py} +0 -0
  475. /ccproxy/{claude_sdk → plugins/claude_sdk}/exceptions.py +0 -0
  476. /ccproxy/{docker → plugins/docker}/models.py +0 -0
  477. /ccproxy/{docker → plugins/docker}/protocol.py +0 -0
  478. /ccproxy/{docker → plugins/docker}/validators.py +0 -0
  479. /ccproxy/{auth/oauth/storage.py → plugins/permissions/handlers/__init__.py} +0 -0
  480. /ccproxy/{api → plugins/permissions}/ui/__init__.py +0 -0
  481. {ccproxy_api-0.1.6.dist-info → ccproxy_api-0.2.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,342 @@
1
+ """Centralized HTML templates for OAuth responses."""
2
+
3
+ from enum import Enum
4
+ from typing import Any
5
+
6
+ from fastapi.responses import HTMLResponse
7
+
8
+
9
+ class OAuthProvider(Enum):
10
+ """OAuth provider types."""
11
+
12
+ CLAUDE = "Claude"
13
+ OPENAI = "OpenAI"
14
+ GENERIC = "OAuth Provider"
15
+
16
+
17
+ class OAuthTemplates:
18
+ """Centralized HTML templates for OAuth responses.
19
+
20
+ This class provides consistent HTML responses across all OAuth providers,
21
+ reducing code duplication and ensuring a uniform user experience.
22
+ """
23
+
24
+ # Base HTML template with common styling
25
+ _BASE_TEMPLATE = """
26
+ <!DOCTYPE html>
27
+ <html lang="en">
28
+ <head>
29
+ <meta charset="UTF-8">
30
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
31
+ <title>{title}</title>
32
+ <style>
33
+ body {{
34
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
35
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
36
+ display: flex;
37
+ justify-content: center;
38
+ align-items: center;
39
+ height: 100vh;
40
+ margin: 0;
41
+ padding: 20px;
42
+ box-sizing: border-box;
43
+ }}
44
+ .container {{
45
+ background: white;
46
+ border-radius: 12px;
47
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
48
+ padding: 40px;
49
+ max-width: 500px;
50
+ width: 100%;
51
+ text-align: center;
52
+ }}
53
+ h1 {{
54
+ color: {header_color};
55
+ margin: 0 0 20px 0;
56
+ font-size: 28px;
57
+ font-weight: 600;
58
+ }}
59
+ .icon {{
60
+ font-size: 48px;
61
+ margin-bottom: 20px;
62
+ }}
63
+ p {{
64
+ color: #4a5568;
65
+ font-size: 16px;
66
+ line-height: 1.6;
67
+ margin: 10px 0;
68
+ }}
69
+ .error-detail {{
70
+ background-color: #fef2f2;
71
+ border: 1px solid #fecaca;
72
+ border-radius: 6px;
73
+ padding: 12px;
74
+ margin-top: 20px;
75
+ color: #991b1b;
76
+ font-family: 'Courier New', Courier, monospace;
77
+ font-size: 14px;
78
+ text-align: left;
79
+ word-wrap: break-word;
80
+ }}
81
+ .success-message {{
82
+ background-color: #f0fdf4;
83
+ border: 1px solid #86efac;
84
+ border-radius: 6px;
85
+ padding: 12px;
86
+ margin-top: 20px;
87
+ color: #166534;
88
+ }}
89
+ .countdown {{
90
+ color: #6b7280;
91
+ font-size: 14px;
92
+ margin-top: 20px;
93
+ }}
94
+ .action-hint {{
95
+ color: #9ca3af;
96
+ font-size: 14px;
97
+ margin-top: 15px;
98
+ }}
99
+ </style>
100
+ </head>
101
+ <body>
102
+ <div class="container">
103
+ {content}
104
+ </div>
105
+ {script}
106
+ </body>
107
+ </html>
108
+ """
109
+
110
+ # Success content template
111
+ _SUCCESS_CONTENT = """
112
+ <div class="icon">✅</div>
113
+ <h1>Authentication Successful!</h1>
114
+ <p>You have successfully authenticated with {provider}.</p>
115
+ <div class="success-message">
116
+ Your credentials have been saved securely.
117
+ </div>
118
+ <p class="action-hint">You can close this window and return to the terminal.</p>
119
+ <div class="countdown" id="countdown">This window will close automatically in 3 seconds...</div>
120
+ """
121
+
122
+ # Error content template
123
+ _ERROR_CONTENT = """
124
+ <div class="icon">❌</div>
125
+ <h1>{title}</h1>
126
+ <p>{message}</p>
127
+ {error_detail}
128
+ <p class="action-hint">You can close this window and try again.</p>
129
+ <div class="countdown" id="countdown">This window will close automatically in 5 seconds...</div>
130
+ """
131
+
132
+ # Auto-close script
133
+ _AUTO_CLOSE_SCRIPT = """
134
+ <script>
135
+ let seconds = {seconds};
136
+ const countdownEl = document.getElementById('countdown');
137
+
138
+ const updateCountdown = () => {{
139
+ if (seconds > 0) {{
140
+ countdownEl.textContent = `This window will close automatically in ${{seconds}} second${{seconds === 1 ? '' : 's'}}...`;
141
+ seconds--;
142
+ setTimeout(updateCountdown, 1000);
143
+ }} else {{
144
+ countdownEl.textContent = 'Closing window...';
145
+ window.close();
146
+ }}
147
+ }};
148
+
149
+ updateCountdown();
150
+
151
+ // Try to close even if countdown doesn't work
152
+ setTimeout(() => {{
153
+ window.close();
154
+ }}, {milliseconds});
155
+ </script>
156
+ """
157
+
158
+ @classmethod
159
+ def success(
160
+ cls,
161
+ provider: OAuthProvider = OAuthProvider.GENERIC,
162
+ auto_close_seconds: int = 3,
163
+ **kwargs: Any,
164
+ ) -> HTMLResponse:
165
+ """Generate success HTML response.
166
+
167
+ Args:
168
+ provider: OAuth provider name
169
+ auto_close_seconds: Seconds before auto-closing window
170
+ **kwargs: Additional template variables
171
+
172
+ Returns:
173
+ HTML response for successful authentication
174
+ """
175
+ content = cls._SUCCESS_CONTENT.format(provider=provider.value, **kwargs)
176
+
177
+ script = cls._AUTO_CLOSE_SCRIPT.format(
178
+ seconds=auto_close_seconds, milliseconds=auto_close_seconds * 1000
179
+ )
180
+
181
+ html = cls._BASE_TEMPLATE.format(
182
+ title="Authentication Successful",
183
+ header_color="#10b981",
184
+ content=content,
185
+ script=script,
186
+ )
187
+
188
+ return HTMLResponse(content=html, status_code=200)
189
+
190
+ @classmethod
191
+ def error(
192
+ cls,
193
+ error_message: str,
194
+ title: str = "Authentication Failed",
195
+ error_detail: str | None = None,
196
+ status_code: int = 400,
197
+ auto_close_seconds: int = 5,
198
+ **kwargs: Any,
199
+ ) -> HTMLResponse:
200
+ """Generate error HTML response.
201
+
202
+ Args:
203
+ error_message: Main error message to display
204
+ title: Page and header title
205
+ error_detail: Optional detailed error information
206
+ status_code: HTTP status code
207
+ auto_close_seconds: Seconds before auto-closing window
208
+ **kwargs: Additional template variables
209
+
210
+ Returns:
211
+ HTML response for failed authentication
212
+ """
213
+ error_detail_html = ""
214
+ if error_detail:
215
+ # Sanitize error detail to prevent XSS
216
+ safe_detail = cls._sanitize_html(error_detail)
217
+ error_detail_html = f'<div class="error-detail">{safe_detail}</div>'
218
+
219
+ content = cls._ERROR_CONTENT.format(
220
+ title=title,
221
+ message=error_message,
222
+ error_detail=error_detail_html,
223
+ **kwargs,
224
+ )
225
+
226
+ script = cls._AUTO_CLOSE_SCRIPT.format(
227
+ seconds=auto_close_seconds, milliseconds=auto_close_seconds * 1000
228
+ )
229
+
230
+ html = cls._BASE_TEMPLATE.format(
231
+ title=title, header_color="#ef4444", content=content, script=script
232
+ )
233
+
234
+ return HTMLResponse(content=html, status_code=status_code)
235
+
236
+ @classmethod
237
+ def callback_error(
238
+ cls,
239
+ error: str | None = None,
240
+ error_description: str | None = None,
241
+ provider: OAuthProvider = OAuthProvider.GENERIC,
242
+ **kwargs: Any,
243
+ ) -> HTMLResponse:
244
+ """Generate error response for OAuth callback errors.
245
+
246
+ Args:
247
+ error: OAuth error code
248
+ error_description: OAuth error description
249
+ provider: OAuth provider name
250
+ **kwargs: Additional template variables
251
+
252
+ Returns:
253
+ HTML response for callback errors
254
+ """
255
+ if error == "access_denied":
256
+ return cls.error(
257
+ error_message=f"You denied access to {provider.value}.",
258
+ title="Access Denied",
259
+ error_detail=error_description,
260
+ **kwargs,
261
+ )
262
+ elif error == "invalid_request":
263
+ return cls.error(
264
+ error_message="The authentication request was invalid.",
265
+ title="Invalid Request",
266
+ error_detail=error_description
267
+ or "The OAuth request parameters were incorrect.",
268
+ **kwargs,
269
+ )
270
+ elif error == "unauthorized_client":
271
+ return cls.error(
272
+ error_message="This application is not authorized.",
273
+ title="Unauthorized Application",
274
+ error_detail=error_description
275
+ or "The client is not authorized to use this grant type.",
276
+ **kwargs,
277
+ )
278
+ elif error == "unsupported_response_type":
279
+ return cls.error(
280
+ error_message="The authorization server does not support this response type.",
281
+ title="Unsupported Response Type",
282
+ error_detail=error_description,
283
+ **kwargs,
284
+ )
285
+ elif error == "invalid_scope":
286
+ return cls.error(
287
+ error_message="The requested scope is invalid or unknown.",
288
+ title="Invalid Scope",
289
+ error_detail=error_description,
290
+ **kwargs,
291
+ )
292
+ elif error == "server_error":
293
+ return cls.error(
294
+ error_message=f"The {provider.value} server encountered an error.",
295
+ title="Server Error",
296
+ error_detail=error_description or "Please try again later.",
297
+ status_code=500,
298
+ **kwargs,
299
+ )
300
+ elif error == "temporarily_unavailable":
301
+ return cls.error(
302
+ error_message=f"The {provider.value} service is temporarily unavailable.",
303
+ title="Service Unavailable",
304
+ error_detail=error_description or "Please try again later.",
305
+ status_code=503,
306
+ **kwargs,
307
+ )
308
+ else:
309
+ # Generic error
310
+ return cls.error(
311
+ error_message=error_description
312
+ or error
313
+ or "An unknown error occurred.",
314
+ title="Authentication Error",
315
+ error_detail=f"Error code: {error}" if error else None,
316
+ **kwargs,
317
+ )
318
+
319
+ @classmethod
320
+ def _sanitize_html(cls, text: str) -> str:
321
+ """Sanitize text for safe HTML display.
322
+
323
+ Args:
324
+ text: Text to sanitize
325
+
326
+ Returns:
327
+ Sanitized text safe for HTML display
328
+ """
329
+ # Basic HTML entity escaping
330
+ replacements = {
331
+ "&": "&amp;",
332
+ "<": "&lt;",
333
+ ">": "&gt;",
334
+ '"': "&quot;",
335
+ "'": "&#x27;",
336
+ "/": "&#x2F;",
337
+ }
338
+
339
+ for char, entity in replacements.items():
340
+ text = text.replace(char, entity)
341
+
342
+ return text
@@ -1,12 +1,9 @@
1
1
  """Token storage implementations for authentication."""
2
2
 
3
- from ccproxy.auth.storage.base import TokenStorage
4
- from ccproxy.auth.storage.json_file import JsonFileTokenStorage
5
- from ccproxy.auth.storage.keyring import KeyringTokenStorage
3
+ from ccproxy.auth.storage.base import BaseJsonStorage, TokenStorage
6
4
 
7
5
 
8
6
  __all__ = [
9
7
  "TokenStorage",
10
- "JsonFileTokenStorage",
11
- "KeyringTokenStorage",
8
+ "BaseJsonStorage",
12
9
  ]
@@ -1,15 +1,33 @@
1
1
  """Abstract base class for token storage."""
2
2
 
3
+ import asyncio
4
+ import contextlib
5
+ import json
6
+ import shutil
3
7
  from abc import ABC, abstractmethod
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Any, Generic, TypeVar
4
11
 
5
- from ccproxy.auth.models import ClaudeCredentials
12
+ from ccproxy.auth.exceptions import CredentialsInvalidError, CredentialsStorageError
13
+ from ccproxy.auth.models.credentials import BaseCredentials
14
+ from ccproxy.core.logging import get_logger
6
15
 
7
16
 
8
- class TokenStorage(ABC):
9
- """Abstract interface for token storage operations."""
17
+ logger = get_logger(__name__)
18
+
19
+ CredentialsT = TypeVar("CredentialsT", bound=BaseCredentials)
20
+
21
+
22
+ class TokenStorage(ABC, Generic[CredentialsT]):
23
+ """Abstract interface for token storage operations.
24
+
25
+ This is a generic interface that can work with any credential type
26
+ that extends BaseModel (e.g., ClaudeCredentials, OpenAICredentials).
27
+ """
10
28
 
11
29
  @abstractmethod
12
- async def load(self) -> ClaudeCredentials | None:
30
+ async def load(self) -> CredentialsT | None:
13
31
  """Load credentials from storage.
14
32
 
15
33
  Returns:
@@ -18,7 +36,7 @@ class TokenStorage(ABC):
18
36
  pass
19
37
 
20
38
  @abstractmethod
21
- async def save(self, credentials: ClaudeCredentials) -> bool:
39
+ async def save(self, credentials: CredentialsT) -> bool:
22
40
  """Save credentials to storage.
23
41
 
24
42
  Args:
@@ -55,3 +73,259 @@ class TokenStorage(ABC):
55
73
  Human-readable description of where credentials are stored
56
74
  """
57
75
  pass
76
+
77
+
78
+ class BaseJsonStorage(TokenStorage[CredentialsT], Generic[CredentialsT]):
79
+ """Base class for JSON file storage implementations.
80
+
81
+ This class provides common JSON read/write operations with error handling,
82
+ atomic writes, and proper permission management.
83
+
84
+ This is a generic class that can work with any credential type.
85
+ """
86
+
87
+ def __init__(self, file_path: Path, enable_backups: bool = True):
88
+ """Initialize JSON storage.
89
+
90
+ Args:
91
+ file_path: Path to JSON file for storage
92
+ enable_backups: Whether to create backups before overwriting
93
+ """
94
+ self.file_path = file_path
95
+ self.enable_backups = enable_backups
96
+
97
+ async def _read_json(self) -> dict[str, Any]:
98
+ """Read JSON data from file with error handling.
99
+
100
+ Returns:
101
+ Parsed JSON data or empty dict if file doesn't exist
102
+
103
+ Raises:
104
+ CredentialsInvalidError: If JSON is invalid
105
+ CredentialsStorageError: If file cannot be read
106
+ """
107
+ if not await self.exists():
108
+ return {}
109
+
110
+ try:
111
+ # Run file I/O in thread pool to avoid blocking
112
+ def read_file() -> dict[str, Any]:
113
+ with self.file_path.open("r") as f:
114
+ return json.load(f) # type: ignore[no-any-return]
115
+
116
+ data = await asyncio.to_thread(read_file)
117
+ return data
118
+
119
+ except json.JSONDecodeError as e:
120
+ logger.warning(
121
+ "json_decode_error",
122
+ path=str(self.file_path),
123
+ error=str(e),
124
+ line=e.lineno,
125
+ category="auth",
126
+ )
127
+ raise CredentialsInvalidError(
128
+ f"Invalid JSON in {self.file_path}: {e}"
129
+ ) from e
130
+
131
+ except FileNotFoundError:
132
+ # File was deleted between exists() check and read
133
+ return {}
134
+
135
+ except PermissionError as e:
136
+ logger.error(
137
+ "permission_denied",
138
+ path=str(self.file_path),
139
+ error=str(e),
140
+ exc_info=e,
141
+ )
142
+ raise CredentialsStorageError(f"Permission denied: {self.file_path}") from e
143
+
144
+ except OSError as e:
145
+ logger.error(
146
+ "file_read_error",
147
+ path=str(self.file_path),
148
+ error=str(e),
149
+ exc_info=e,
150
+ )
151
+ raise CredentialsStorageError(f"Error reading {self.file_path}: {e}") from e
152
+
153
+ async def _create_backup(self) -> bool:
154
+ """Create a timestamped backup of the current file.
155
+
156
+ Returns:
157
+ True if backup was created successfully, False otherwise
158
+ """
159
+ if not await self.exists():
160
+ return False
161
+
162
+ try:
163
+ # Generate backup filename with timestamp
164
+ timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
165
+ backup_name = f"{self.file_path.name}.{timestamp}.bak"
166
+ backup_path = self.file_path.parent / backup_name
167
+
168
+ # Copy file to backup location
169
+ await asyncio.to_thread(shutil.copy2, self.file_path, backup_path)
170
+
171
+ logger.debug(
172
+ "backup_created",
173
+ original=str(self.file_path),
174
+ backup=str(backup_path),
175
+ category="auth",
176
+ )
177
+ return True
178
+
179
+ except Exception as e:
180
+ logger.warning(
181
+ "backup_failed",
182
+ path=str(self.file_path),
183
+ error=str(e),
184
+ exc_info=e,
185
+ category="auth",
186
+ )
187
+ return False
188
+
189
+ async def _write_json(self, data: dict[str, Any]) -> None:
190
+ """Write JSON data to file atomically with error handling.
191
+
192
+ This method performs atomic writes by writing to a temporary file
193
+ first, then renaming it to the target file. If backups are enabled
194
+ and the file exists, a backup will be created before overwriting.
195
+
196
+ Args:
197
+ data: Data to write as JSON
198
+
199
+ Raises:
200
+ CredentialsStorageError: If file cannot be written
201
+ """
202
+ # Create backup if enabled and file exists
203
+ if self.enable_backups and await self.exists():
204
+ await self._create_backup()
205
+
206
+ temp_path = self.file_path.with_suffix(".tmp")
207
+
208
+ try:
209
+ # Ensure parent directory exists
210
+ await asyncio.to_thread(
211
+ self.file_path.parent.mkdir,
212
+ parents=True,
213
+ exist_ok=True,
214
+ )
215
+
216
+ # Run file I/O in thread pool to avoid blocking
217
+ def write_file() -> None:
218
+ # Write to temporary file
219
+ with temp_path.open("w") as f:
220
+ json.dump(data, f, indent=2)
221
+
222
+ # Set restrictive permissions (read/write for owner only)
223
+ temp_path.chmod(0o600)
224
+
225
+ # Atomic rename
226
+ temp_path.replace(self.file_path)
227
+
228
+ await asyncio.to_thread(write_file)
229
+
230
+ logger.debug(
231
+ "json_write_success",
232
+ path=str(self.file_path),
233
+ size=len(json.dumps(data)),
234
+ )
235
+
236
+ except (TypeError, ValueError) as e:
237
+ logger.error(
238
+ "json_encode_error",
239
+ path=str(self.file_path),
240
+ error=str(e),
241
+ exc_info=e,
242
+ )
243
+ raise CredentialsStorageError(f"Failed to encode JSON: {e}") from e
244
+
245
+ except PermissionError as e:
246
+ logger.error(
247
+ "permission_denied",
248
+ path=str(self.file_path),
249
+ error=str(e),
250
+ exc_info=e,
251
+ )
252
+ raise CredentialsStorageError(f"Permission denied: {self.file_path}") from e
253
+
254
+ except OSError as e:
255
+ logger.error(
256
+ "file_write_error",
257
+ path=str(self.file_path),
258
+ error=str(e),
259
+ exc_info=e,
260
+ )
261
+ raise CredentialsStorageError(f"Error writing {self.file_path}: {e}") from e
262
+
263
+ finally:
264
+ # Clean up temp file if it exists
265
+ if temp_path.exists():
266
+ with contextlib.suppress(Exception):
267
+ temp_path.unlink()
268
+
269
+ async def exists(self) -> bool:
270
+ """Check if credentials file exists.
271
+
272
+ Returns:
273
+ True if file exists, False otherwise
274
+ """
275
+ # Run file system check in thread pool for consistency
276
+ file_exists = await asyncio.to_thread(
277
+ lambda: self.file_path.exists() and self.file_path.is_file()
278
+ )
279
+
280
+ logger.debug(
281
+ "auth_file_existence_check",
282
+ file_path=str(self.file_path),
283
+ exists=file_exists,
284
+ category="auth",
285
+ )
286
+
287
+ return file_exists
288
+
289
+ async def delete(self) -> bool:
290
+ """Delete credentials file.
291
+
292
+ Returns:
293
+ True if deleted successfully, False if file didn't exist
294
+
295
+ Raises:
296
+ CredentialsStorageError: If file cannot be deleted
297
+ """
298
+ try:
299
+ if await self.exists():
300
+ await asyncio.to_thread(self.file_path.unlink)
301
+ logger.debug("file_deleted", path=str(self.file_path))
302
+ return True
303
+ return False
304
+
305
+ except PermissionError as e:
306
+ logger.error(
307
+ "permission_denied",
308
+ path=str(self.file_path),
309
+ error=str(e),
310
+ exc_info=e,
311
+ )
312
+ raise CredentialsStorageError(f"Permission denied: {self.file_path}") from e
313
+
314
+ except OSError as e:
315
+ logger.error(
316
+ "file_delete_error",
317
+ path=str(self.file_path),
318
+ error=str(e),
319
+ exc_info=e,
320
+ )
321
+ raise CredentialsStorageError(
322
+ f"Error deleting {self.file_path}: {e}"
323
+ ) from e
324
+
325
+ def get_location(self) -> str:
326
+ """Get the storage location description.
327
+
328
+ Returns:
329
+ Path to the JSON file
330
+ """
331
+ return str(self.file_path)