codex-lb 0.5.0__tar.gz → 0.5.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {codex_lb-0.5.0 → codex_lb-0.5.1}/.all-contributorsrc +11 -0
- codex_lb-0.5.1/.github/release-please-manifest.json +3 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/CHANGELOG.md +12 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/PKG-INFO +2 -1
- {codex_lb-0.5.0 → codex_lb-0.5.1}/README.md +1 -0
- codex_lb-0.5.1/app/core/middleware/request_decompression.py +158 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/pyproject.toml +1 -1
- codex_lb-0.5.1/tests/unit/test_request_decompression_middleware.py +165 -0
- codex_lb-0.5.0/.github/release-please-manifest.json +0 -3
- codex_lb-0.5.0/app/core/middleware/request_decompression.py +0 -101
- codex_lb-0.5.0/tests/unit/test_request_decompression_middleware.py +0 -45
- {codex_lb-0.5.0 → codex_lb-0.5.1}/.dockerignore +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/.env.example +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/.github/release-please-config.json +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/.github/workflows/ci.yml +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/.github/workflows/release-please.yml +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/.github/workflows/release.yml +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/.gitignore +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/.pre-commit-config.yaml +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/AGENTS.md +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/Dockerfile +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/LICENSE +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/cli.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/auth/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/auth/models.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/auth/refresh.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/balancer/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/balancer/logic.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/balancer/types.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/clients/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/clients/http.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/clients/oauth.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/clients/proxy.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/clients/usage.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/config/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/config/settings.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/crypto.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/errors.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/handlers/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/handlers/exceptions.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/middleware/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/middleware/api_errors.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/middleware/request_id.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/openai/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/openai/chat_requests.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/openai/chat_responses.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/openai/message_coercion.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/openai/models.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/openai/models_catalog.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/openai/parsing.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/openai/requests.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/openai/v1_requests.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/plan_types.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/types.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/usage/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/usage/logs.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/usage/models.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/usage/pricing.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/usage/quota.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/usage/types.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/utils/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/utils/request_id.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/utils/retry.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/utils/sse.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/core/utils/time.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/db/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/db/migrations/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/db/migrations/versions/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/db/migrations/versions/add_accounts_chatgpt_account_id.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/db/migrations/versions/add_accounts_reset_at.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/db/migrations/versions/add_dashboard_settings.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/db/migrations/versions/add_request_logs_reasoning_effort.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/db/migrations/versions/normalize_account_plan_types.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/db/models.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/db/session.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/dependencies.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/main.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/accounts/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/accounts/api.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/accounts/auth_manager.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/accounts/repository.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/accounts/schemas.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/accounts/service.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/health/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/health/api.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/health/schemas.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/oauth/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/oauth/api.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/oauth/schemas.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/oauth/service.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/oauth/templates/oauth_success.html +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/proxy/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/proxy/api.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/proxy/helpers.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/proxy/load_balancer.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/proxy/repo_bundle.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/proxy/schemas.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/proxy/service.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/proxy/sticky_repository.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/proxy/types.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/request_logs/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/request_logs/api.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/request_logs/repository.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/request_logs/schemas.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/request_logs/service.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/settings/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/settings/api.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/settings/repository.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/settings/schemas.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/settings/service.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/shared/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/shared/schemas.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/usage/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/usage/api.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/usage/repository.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/usage/schemas.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/usage/service.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/modules/usage/updater.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/static/index.css +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/static/index.html +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/app/static/index.js +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/docker-compose.yml +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/docs/plans/2026-01-27-v1-chat-bridge-implementation.md +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/docs/screenshots/accounts.jpg +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/docs/screenshots/dashboard.jpg +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/__init__.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/conftest.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/integration/test_accounts_api.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/integration/test_accounts_api_extended.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/integration/test_codex_usage_api.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/integration/test_db_models.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/integration/test_health_and_errors.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/integration/test_load_balancer_integration.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/integration/test_migrations.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/integration/test_oauth_flow.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/integration/test_proxy_api_extended.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/integration/test_proxy_chat_completions.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/integration/test_proxy_compact.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/integration/test_proxy_responses.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/integration/test_proxy_sticky_sessions.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/integration/test_repositories.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/integration/test_request_decompression.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/integration/test_request_logs_api.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/integration/test_request_logs_filters.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/integration/test_settings_api.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/integration/test_usage_api.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/integration/test_usage_summary.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/integration/test_v1_models.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/test_request_logs_options_api.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/unit/test_auth.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/unit/test_auth_manager.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/unit/test_auth_refresh.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/unit/test_chat_request_mapping.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/unit/test_chat_response_mapping.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/unit/test_db_session.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/unit/test_load_balancer.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/unit/test_oauth_client.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/unit/test_openai_requests.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/unit/test_pricing.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/unit/test_proxy_errors.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/unit/test_proxy_utils.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/unit/test_request_logs_repository.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/unit/test_retry.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/unit/test_sse.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/unit/test_usage.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/unit/test_usage_client.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/tests/unit/test_usage_updater.py +0 -0
- {codex_lb-0.5.0 → codex_lb-0.5.1}/uv.lock +0 -0
|
@@ -64,6 +64,17 @@
|
|
|
64
64
|
"contributions": [
|
|
65
65
|
"doc"
|
|
66
66
|
]
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"login": "choi138",
|
|
70
|
+
"name": "Choi138",
|
|
71
|
+
"avatar_url": "https://avatars.githubusercontent.com/u/84369321?v=4",
|
|
72
|
+
"profile": "https://github.com/choi138",
|
|
73
|
+
"contributions": [
|
|
74
|
+
"code",
|
|
75
|
+
"bug",
|
|
76
|
+
"test"
|
|
77
|
+
]
|
|
67
78
|
}
|
|
68
79
|
],
|
|
69
80
|
"contributorsPerLine": 7,
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.5.1](https://github.com/Soju06/codex-lb/compare/v0.5.0...v0.5.1) (2026-02-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
* **core:** support gzip/deflate request decompression ([#49](https://github.com/Soju06/codex-lb/issues/49)) ([1db79aa](https://github.com/Soju06/codex-lb/commit/1db79aaef8d65af4b9246fad2b0687be17daba6b))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Documentation
|
|
12
|
+
|
|
13
|
+
* add choi138 as a contributor for code, bug, and test ([#50](https://github.com/Soju06/codex-lb/issues/50)) ([80d5aae](https://github.com/Soju06/codex-lb/commit/80d5aaefd5c61ea420fda90744e8ffda69eaecf6))
|
|
14
|
+
|
|
3
15
|
## [0.5.0](https://github.com/Soju06/codex-lb/compare/v0.4.0...v0.5.0) (2026-01-29)
|
|
4
16
|
|
|
5
17
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-lb
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: Codex load balancer and proxy for ChatGPT accounts with usage dashboard
|
|
5
5
|
Author-email: Soju06 <qlskssk@gmail.com>
|
|
6
6
|
Maintainer-email: Soju06 <qlskssk@gmail.com>
|
|
@@ -161,6 +161,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|
|
161
161
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Quack6765"><img src="https://avatars.githubusercontent.com/u/5446230?v=4?s=100" width="100px;" alt="Quack"/><br /><sub><b>Quack</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=Quack6765" title="Code">💻</a> <a href="https://github.com/Soju06/codex-lb/issues?q=author%3AQuack6765" title="Bug reports">🐛</a> <a href="#maintenance-Quack6765" title="Maintenance">🚧</a> <a href="#design-Quack6765" title="Design">🎨</a></td>
|
|
162
162
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hhsw2015"><img src="https://avatars.githubusercontent.com/u/103614420?v=4?s=100" width="100px;" alt="Jill Kok, San Mou"/><br /><sub><b>Jill Kok, San Mou</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=hhsw2015" title="Code">💻</a> <a href="https://github.com/Soju06/codex-lb/commits?author=hhsw2015" title="Tests">⚠️</a> <a href="#maintenance-hhsw2015" title="Maintenance">🚧</a></td>
|
|
163
163
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pcy06"><img src="https://avatars.githubusercontent.com/u/44970486?v=4?s=100" width="100px;" alt="PARK CHANYOUNG"/><br /><sub><b>PARK CHANYOUNG</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=pcy06" title="Documentation">📖</a></td>
|
|
164
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/choi138"><img src="https://avatars.githubusercontent.com/u/84369321?v=4?s=100" width="100px;" alt="Choi138"/><br /><sub><b>Choi138</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=choi138" title="Code">💻</a> <a href="https://github.com/Soju06/codex-lb/issues?q=author%3Achoi138" title="Bug reports">🐛</a> <a href="https://github.com/Soju06/codex-lb/commits?author=choi138" title="Tests">⚠️</a></td>
|
|
164
165
|
</tr>
|
|
165
166
|
</tbody>
|
|
166
167
|
</table>
|
|
@@ -107,6 +107,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
|
|
|
107
107
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/Quack6765"><img src="https://avatars.githubusercontent.com/u/5446230?v=4?s=100" width="100px;" alt="Quack"/><br /><sub><b>Quack</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=Quack6765" title="Code">💻</a> <a href="https://github.com/Soju06/codex-lb/issues?q=author%3AQuack6765" title="Bug reports">🐛</a> <a href="#maintenance-Quack6765" title="Maintenance">🚧</a> <a href="#design-Quack6765" title="Design">🎨</a></td>
|
|
108
108
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/hhsw2015"><img src="https://avatars.githubusercontent.com/u/103614420?v=4?s=100" width="100px;" alt="Jill Kok, San Mou"/><br /><sub><b>Jill Kok, San Mou</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=hhsw2015" title="Code">💻</a> <a href="https://github.com/Soju06/codex-lb/commits?author=hhsw2015" title="Tests">⚠️</a> <a href="#maintenance-hhsw2015" title="Maintenance">🚧</a></td>
|
|
109
109
|
<td align="center" valign="top" width="14.28%"><a href="https://github.com/pcy06"><img src="https://avatars.githubusercontent.com/u/44970486?v=4?s=100" width="100px;" alt="PARK CHANYOUNG"/><br /><sub><b>PARK CHANYOUNG</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=pcy06" title="Documentation">📖</a></td>
|
|
110
|
+
<td align="center" valign="top" width="14.28%"><a href="https://github.com/choi138"><img src="https://avatars.githubusercontent.com/u/84369321?v=4?s=100" width="100px;" alt="Choi138"/><br /><sub><b>Choi138</b></sub></a><br /><a href="https://github.com/Soju06/codex-lb/commits?author=choi138" title="Code">💻</a> <a href="https://github.com/Soju06/codex-lb/issues?q=author%3Achoi138" title="Bug reports">🐛</a> <a href="https://github.com/Soju06/codex-lb/commits?author=choi138" title="Tests">⚠️</a></td>
|
|
110
111
|
</tr>
|
|
111
112
|
</tbody>
|
|
112
113
|
</table>
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import gzip
|
|
4
|
+
import io
|
|
5
|
+
import zlib
|
|
6
|
+
from collections.abc import Awaitable, Callable
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
|
|
9
|
+
import zstandard as zstd
|
|
10
|
+
from fastapi import FastAPI, Request
|
|
11
|
+
from fastapi.responses import JSONResponse, Response
|
|
12
|
+
|
|
13
|
+
from app.core.config.settings import get_settings
|
|
14
|
+
from app.core.errors import dashboard_error
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class _DecompressedBodyTooLarge(Exception):
|
|
18
|
+
def __init__(self, max_size: int) -> None:
|
|
19
|
+
super().__init__(f"Decompressed body exceeded {max_size} bytes")
|
|
20
|
+
self.max_size = max_size
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _Readable(Protocol):
|
|
24
|
+
def read(self, size: int = ...) -> bytes: ...
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _read_limited(reader: _Readable, max_size: int) -> bytes:
|
|
28
|
+
buffer = bytearray()
|
|
29
|
+
total = 0
|
|
30
|
+
chunk_size = 64 * 1024
|
|
31
|
+
while True:
|
|
32
|
+
chunk = reader.read(chunk_size)
|
|
33
|
+
if not chunk:
|
|
34
|
+
break
|
|
35
|
+
total += len(chunk)
|
|
36
|
+
if total > max_size:
|
|
37
|
+
raise _DecompressedBodyTooLarge(max_size)
|
|
38
|
+
buffer.extend(chunk)
|
|
39
|
+
return bytes(buffer)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _decompress_gzip(data: bytes, max_size: int) -> bytes:
|
|
43
|
+
with gzip.GzipFile(fileobj=io.BytesIO(data)) as reader:
|
|
44
|
+
return _read_limited(reader, max_size)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _decompress_deflate(data: bytes, max_size: int) -> bytes:
|
|
48
|
+
decompressor = zlib.decompressobj()
|
|
49
|
+
buffer = bytearray()
|
|
50
|
+
chunk_size = 64 * 1024
|
|
51
|
+
for start in range(0, len(data), chunk_size):
|
|
52
|
+
chunk = data[start : start + chunk_size]
|
|
53
|
+
# Bound output growth to avoid oversized allocations.
|
|
54
|
+
while chunk:
|
|
55
|
+
remaining = max_size - len(buffer)
|
|
56
|
+
if remaining == 0:
|
|
57
|
+
raise _DecompressedBodyTooLarge(max_size)
|
|
58
|
+
buffer.extend(decompressor.decompress(chunk, max_length=remaining))
|
|
59
|
+
chunk = decompressor.unconsumed_tail
|
|
60
|
+
while True:
|
|
61
|
+
remaining = max_size - len(buffer)
|
|
62
|
+
if remaining == 0:
|
|
63
|
+
raise _DecompressedBodyTooLarge(max_size)
|
|
64
|
+
drained = decompressor.decompress(b"", max_length=remaining)
|
|
65
|
+
if not drained:
|
|
66
|
+
break
|
|
67
|
+
buffer.extend(drained)
|
|
68
|
+
if not decompressor.eof:
|
|
69
|
+
raise zlib.error("Incomplete deflate stream")
|
|
70
|
+
return bytes(buffer)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _decompress_zstd(data: bytes, max_size: int) -> bytes:
|
|
74
|
+
try:
|
|
75
|
+
decompressed = zstd.ZstdDecompressor().decompress(data, max_output_size=max_size)
|
|
76
|
+
if len(decompressed) > max_size:
|
|
77
|
+
raise _DecompressedBodyTooLarge(max_size)
|
|
78
|
+
return decompressed
|
|
79
|
+
except _DecompressedBodyTooLarge:
|
|
80
|
+
raise
|
|
81
|
+
except Exception:
|
|
82
|
+
with zstd.ZstdDecompressor().stream_reader(io.BytesIO(data)) as reader:
|
|
83
|
+
return _read_limited(reader, max_size)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _decompress_body(data: bytes, encodings: list[str], max_size: int) -> bytes:
|
|
87
|
+
supported = {"zstd", "gzip", "deflate", "identity"}
|
|
88
|
+
if any(encoding not in supported for encoding in encodings):
|
|
89
|
+
raise ValueError("Unsupported content-encoding")
|
|
90
|
+
result = data
|
|
91
|
+
for encoding in reversed(encodings):
|
|
92
|
+
if encoding == "zstd":
|
|
93
|
+
result = _decompress_zstd(result, max_size)
|
|
94
|
+
elif encoding == "gzip":
|
|
95
|
+
result = _decompress_gzip(result, max_size)
|
|
96
|
+
elif encoding == "deflate":
|
|
97
|
+
result = _decompress_deflate(result, max_size)
|
|
98
|
+
elif encoding == "identity":
|
|
99
|
+
continue
|
|
100
|
+
return result
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _replace_request_body(request: Request, body: bytes) -> None:
|
|
104
|
+
request._body = body
|
|
105
|
+
headers: list[tuple[bytes, bytes]] = []
|
|
106
|
+
for key, value in request.scope.get("headers", []):
|
|
107
|
+
if key.lower() in (b"content-encoding", b"content-length"):
|
|
108
|
+
continue
|
|
109
|
+
headers.append((key, value))
|
|
110
|
+
headers.append((b"content-length", str(len(body)).encode("ascii")))
|
|
111
|
+
request.scope["headers"] = headers
|
|
112
|
+
# Ensure subsequent request.headers reflects the updated scope headers.
|
|
113
|
+
request._headers = None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def add_request_decompression_middleware(app: FastAPI) -> None:
|
|
117
|
+
@app.middleware("http")
|
|
118
|
+
async def request_decompression_middleware(
|
|
119
|
+
request: Request,
|
|
120
|
+
call_next: Callable[[Request], Awaitable[Response]],
|
|
121
|
+
) -> Response:
|
|
122
|
+
content_encoding = request.headers.get("content-encoding")
|
|
123
|
+
if not content_encoding:
|
|
124
|
+
return await call_next(request)
|
|
125
|
+
encodings = [enc.strip().lower() for enc in content_encoding.split(",") if enc.strip()]
|
|
126
|
+
if not encodings:
|
|
127
|
+
return await call_next(request)
|
|
128
|
+
body = await request.body()
|
|
129
|
+
settings = get_settings()
|
|
130
|
+
max_size = settings.max_decompressed_body_bytes
|
|
131
|
+
try:
|
|
132
|
+
decompressed = _decompress_body(body, encodings, max_size)
|
|
133
|
+
except _DecompressedBodyTooLarge:
|
|
134
|
+
return JSONResponse(
|
|
135
|
+
status_code=413,
|
|
136
|
+
content=dashboard_error(
|
|
137
|
+
"payload_too_large",
|
|
138
|
+
"Request body exceeds the maximum allowed size",
|
|
139
|
+
),
|
|
140
|
+
)
|
|
141
|
+
except ValueError:
|
|
142
|
+
return JSONResponse(
|
|
143
|
+
status_code=400,
|
|
144
|
+
content=dashboard_error(
|
|
145
|
+
"invalid_request",
|
|
146
|
+
"Unsupported Content-Encoding",
|
|
147
|
+
),
|
|
148
|
+
)
|
|
149
|
+
except Exception:
|
|
150
|
+
return JSONResponse(
|
|
151
|
+
status_code=400,
|
|
152
|
+
content=dashboard_error(
|
|
153
|
+
"invalid_request",
|
|
154
|
+
"Request body is compressed but could not be decompressed",
|
|
155
|
+
),
|
|
156
|
+
)
|
|
157
|
+
_replace_request_body(request, decompressed)
|
|
158
|
+
return await call_next(request)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import gzip
|
|
4
|
+
import json
|
|
5
|
+
import zlib
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
import zstandard as zstd
|
|
9
|
+
from fastapi import FastAPI, Request
|
|
10
|
+
from httpx import ASGITransport, AsyncClient
|
|
11
|
+
|
|
12
|
+
from app.core.middleware.request_decompression import add_request_decompression_middleware
|
|
13
|
+
|
|
14
|
+
pytestmark = pytest.mark.unit
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _build_echo_app(*, touch_headers: bool = False) -> FastAPI:
|
|
18
|
+
app = FastAPI()
|
|
19
|
+
add_request_decompression_middleware(app)
|
|
20
|
+
|
|
21
|
+
if touch_headers:
|
|
22
|
+
|
|
23
|
+
@app.middleware("http")
|
|
24
|
+
async def touch_headers_middleware(request: Request, call_next):
|
|
25
|
+
_ = request.headers.get("content-encoding")
|
|
26
|
+
return await call_next(request)
|
|
27
|
+
|
|
28
|
+
@app.post("/echo")
|
|
29
|
+
async def echo(request: Request):
|
|
30
|
+
data = await request.json()
|
|
31
|
+
return {"content_encoding": request.headers.get("content-encoding"), "data": data}
|
|
32
|
+
|
|
33
|
+
return app
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.mark.asyncio
|
|
37
|
+
async def test_request_decompression_clears_cached_headers():
|
|
38
|
+
app = _build_echo_app(touch_headers=True)
|
|
39
|
+
|
|
40
|
+
payload = {"hello": "world"}
|
|
41
|
+
body = json.dumps(payload).encode("utf-8")
|
|
42
|
+
compressed = zstd.ZstdCompressor().compress(body)
|
|
43
|
+
|
|
44
|
+
transport = ASGITransport(app=app)
|
|
45
|
+
async with AsyncClient(transport=transport, base_url="http://testserver") as client:
|
|
46
|
+
resp = await client.post(
|
|
47
|
+
"/echo",
|
|
48
|
+
content=compressed,
|
|
49
|
+
headers={"Content-Encoding": "zstd", "Content-Type": "application/json"},
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
assert resp.status_code == 200
|
|
53
|
+
response_data = resp.json()
|
|
54
|
+
assert response_data["content_encoding"] is None
|
|
55
|
+
assert response_data["data"] == payload
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@pytest.mark.asyncio
|
|
59
|
+
async def test_request_decompression_supports_gzip():
|
|
60
|
+
app = _build_echo_app()
|
|
61
|
+
|
|
62
|
+
payload = {"hello": "gzip"}
|
|
63
|
+
body = json.dumps(payload).encode("utf-8")
|
|
64
|
+
compressed = gzip.compress(body)
|
|
65
|
+
|
|
66
|
+
transport = ASGITransport(app=app)
|
|
67
|
+
async with AsyncClient(transport=transport, base_url="http://testserver") as client:
|
|
68
|
+
resp = await client.post(
|
|
69
|
+
"/echo",
|
|
70
|
+
content=compressed,
|
|
71
|
+
headers={"Content-Encoding": "gzip", "Content-Type": "application/json"},
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
assert resp.status_code == 200
|
|
75
|
+
response_data = resp.json()
|
|
76
|
+
assert response_data["content_encoding"] is None
|
|
77
|
+
assert response_data["data"] == payload
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@pytest.mark.asyncio
|
|
81
|
+
async def test_request_decompression_supports_deflate():
|
|
82
|
+
app = _build_echo_app()
|
|
83
|
+
|
|
84
|
+
payload = {"hello": "deflate"}
|
|
85
|
+
body = json.dumps(payload).encode("utf-8")
|
|
86
|
+
compressed = zlib.compress(body)
|
|
87
|
+
|
|
88
|
+
transport = ASGITransport(app=app)
|
|
89
|
+
async with AsyncClient(transport=transport, base_url="http://testserver") as client:
|
|
90
|
+
resp = await client.post(
|
|
91
|
+
"/echo",
|
|
92
|
+
content=compressed,
|
|
93
|
+
headers={"Content-Encoding": "deflate", "Content-Type": "application/json"},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
assert resp.status_code == 200
|
|
97
|
+
response_data = resp.json()
|
|
98
|
+
assert response_data["content_encoding"] is None
|
|
99
|
+
assert response_data["data"] == payload
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@pytest.mark.asyncio
|
|
103
|
+
async def test_request_decompression_allows_identity():
|
|
104
|
+
app = _build_echo_app()
|
|
105
|
+
|
|
106
|
+
payload = {"hello": "identity"}
|
|
107
|
+
body = json.dumps(payload).encode("utf-8")
|
|
108
|
+
|
|
109
|
+
transport = ASGITransport(app=app)
|
|
110
|
+
async with AsyncClient(transport=transport, base_url="http://testserver") as client:
|
|
111
|
+
resp = await client.post(
|
|
112
|
+
"/echo",
|
|
113
|
+
content=body,
|
|
114
|
+
headers={"Content-Encoding": "identity", "Content-Type": "application/json"},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
assert resp.status_code == 200
|
|
118
|
+
response_data = resp.json()
|
|
119
|
+
assert response_data["content_encoding"] is None
|
|
120
|
+
assert response_data["data"] == payload
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@pytest.mark.asyncio
|
|
124
|
+
async def test_request_decompression_supports_multiple_encodings():
|
|
125
|
+
app = _build_echo_app()
|
|
126
|
+
|
|
127
|
+
payload = {"hello": "multi"}
|
|
128
|
+
body = json.dumps(payload).encode("utf-8")
|
|
129
|
+
gzip_body = gzip.compress(body)
|
|
130
|
+
compressed = zstd.ZstdCompressor().compress(gzip_body)
|
|
131
|
+
|
|
132
|
+
transport = ASGITransport(app=app)
|
|
133
|
+
async with AsyncClient(transport=transport, base_url="http://testserver") as client:
|
|
134
|
+
resp = await client.post(
|
|
135
|
+
"/echo",
|
|
136
|
+
content=compressed,
|
|
137
|
+
headers={"Content-Encoding": "gzip, zstd", "Content-Type": "application/json"},
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
assert resp.status_code == 200
|
|
141
|
+
response_data = resp.json()
|
|
142
|
+
assert response_data["content_encoding"] is None
|
|
143
|
+
assert response_data["data"] == payload
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
@pytest.mark.asyncio
|
|
147
|
+
async def test_request_decompression_rejects_unsupported_encoding():
|
|
148
|
+
app = _build_echo_app()
|
|
149
|
+
|
|
150
|
+
payload = {"hello": "br"}
|
|
151
|
+
body = json.dumps(payload).encode("utf-8")
|
|
152
|
+
compressed = gzip.compress(body)
|
|
153
|
+
|
|
154
|
+
transport = ASGITransport(app=app)
|
|
155
|
+
async with AsyncClient(transport=transport, base_url="http://testserver") as client:
|
|
156
|
+
resp = await client.post(
|
|
157
|
+
"/echo",
|
|
158
|
+
content=compressed,
|
|
159
|
+
headers={"Content-Encoding": "br", "Content-Type": "application/json"},
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
assert resp.status_code == 400
|
|
163
|
+
response_data = resp.json()
|
|
164
|
+
assert response_data["error"]["code"] == "invalid_request"
|
|
165
|
+
assert response_data["error"]["message"] == "Unsupported Content-Encoding"
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import io
|
|
4
|
-
from collections.abc import Awaitable, Callable
|
|
5
|
-
from typing import Protocol
|
|
6
|
-
|
|
7
|
-
import zstandard as zstd
|
|
8
|
-
from fastapi import FastAPI, Request
|
|
9
|
-
from fastapi.responses import JSONResponse, Response
|
|
10
|
-
|
|
11
|
-
from app.core.config.settings import get_settings
|
|
12
|
-
from app.core.errors import dashboard_error
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
class _DecompressedBodyTooLarge(Exception):
|
|
16
|
-
def __init__(self, max_size: int) -> None:
|
|
17
|
-
super().__init__(f"Decompressed body exceeded {max_size} bytes")
|
|
18
|
-
self.max_size = max_size
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
class _Readable(Protocol):
|
|
22
|
-
def read(self, size: int = ...) -> bytes: ...
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def _read_limited(reader: _Readable, max_size: int) -> bytes:
|
|
26
|
-
buffer = bytearray()
|
|
27
|
-
total = 0
|
|
28
|
-
chunk_size = 64 * 1024
|
|
29
|
-
while True:
|
|
30
|
-
chunk = reader.read(chunk_size)
|
|
31
|
-
if not chunk:
|
|
32
|
-
break
|
|
33
|
-
total += len(chunk)
|
|
34
|
-
if total > max_size:
|
|
35
|
-
raise _DecompressedBodyTooLarge(max_size)
|
|
36
|
-
buffer.extend(chunk)
|
|
37
|
-
return bytes(buffer)
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
def _replace_request_body(request: Request, body: bytes) -> None:
|
|
41
|
-
request._body = body
|
|
42
|
-
headers: list[tuple[bytes, bytes]] = []
|
|
43
|
-
for key, value in request.scope.get("headers", []):
|
|
44
|
-
if key.lower() in (b"content-encoding", b"content-length"):
|
|
45
|
-
continue
|
|
46
|
-
headers.append((key, value))
|
|
47
|
-
headers.append((b"content-length", str(len(body)).encode("ascii")))
|
|
48
|
-
request.scope["headers"] = headers
|
|
49
|
-
# Ensure subsequent request.headers reflects the updated scope headers.
|
|
50
|
-
request._headers = None
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def add_request_decompression_middleware(app: FastAPI) -> None:
|
|
54
|
-
@app.middleware("http")
|
|
55
|
-
async def request_decompression_middleware(
|
|
56
|
-
request: Request,
|
|
57
|
-
call_next: Callable[[Request], Awaitable[Response]],
|
|
58
|
-
) -> Response:
|
|
59
|
-
content_encoding = request.headers.get("content-encoding")
|
|
60
|
-
if not content_encoding:
|
|
61
|
-
return await call_next(request)
|
|
62
|
-
encodings = [enc.strip().lower() for enc in content_encoding.split(",") if enc.strip()]
|
|
63
|
-
if encodings != ["zstd"]:
|
|
64
|
-
return await call_next(request)
|
|
65
|
-
body = await request.body()
|
|
66
|
-
settings = get_settings()
|
|
67
|
-
max_size = settings.max_decompressed_body_bytes
|
|
68
|
-
try:
|
|
69
|
-
decompressed = zstd.ZstdDecompressor().decompress(body, max_output_size=max_size)
|
|
70
|
-
if len(decompressed) > max_size:
|
|
71
|
-
raise _DecompressedBodyTooLarge(max_size)
|
|
72
|
-
except _DecompressedBodyTooLarge:
|
|
73
|
-
return JSONResponse(
|
|
74
|
-
status_code=413,
|
|
75
|
-
content=dashboard_error(
|
|
76
|
-
"payload_too_large",
|
|
77
|
-
"Request body exceeds the maximum allowed size",
|
|
78
|
-
),
|
|
79
|
-
)
|
|
80
|
-
except Exception:
|
|
81
|
-
try:
|
|
82
|
-
with zstd.ZstdDecompressor().stream_reader(io.BytesIO(body)) as reader:
|
|
83
|
-
decompressed = _read_limited(reader, max_size)
|
|
84
|
-
except _DecompressedBodyTooLarge:
|
|
85
|
-
return JSONResponse(
|
|
86
|
-
status_code=413,
|
|
87
|
-
content=dashboard_error(
|
|
88
|
-
"payload_too_large",
|
|
89
|
-
"Request body exceeds the maximum allowed size",
|
|
90
|
-
),
|
|
91
|
-
)
|
|
92
|
-
except Exception:
|
|
93
|
-
return JSONResponse(
|
|
94
|
-
status_code=400,
|
|
95
|
-
content=dashboard_error(
|
|
96
|
-
"invalid_request",
|
|
97
|
-
"Request body is zstd-compressed but could not be decompressed",
|
|
98
|
-
),
|
|
99
|
-
)
|
|
100
|
-
_replace_request_body(request, decompressed)
|
|
101
|
-
return await call_next(request)
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
import zstandard as zstd
|
|
7
|
-
from fastapi import FastAPI, Request
|
|
8
|
-
from httpx import ASGITransport, AsyncClient
|
|
9
|
-
|
|
10
|
-
from app.core.middleware.request_decompression import add_request_decompression_middleware
|
|
11
|
-
|
|
12
|
-
pytestmark = pytest.mark.unit
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
@pytest.mark.asyncio
|
|
16
|
-
async def test_request_decompression_clears_cached_headers():
|
|
17
|
-
app = FastAPI()
|
|
18
|
-
add_request_decompression_middleware(app)
|
|
19
|
-
|
|
20
|
-
@app.middleware("http")
|
|
21
|
-
async def touch_headers(request: Request, call_next):
|
|
22
|
-
_ = request.headers.get("content-encoding")
|
|
23
|
-
return await call_next(request)
|
|
24
|
-
|
|
25
|
-
@app.post("/echo")
|
|
26
|
-
async def echo(request: Request):
|
|
27
|
-
data = await request.json()
|
|
28
|
-
return {"content_encoding": request.headers.get("content-encoding"), "data": data}
|
|
29
|
-
|
|
30
|
-
payload = {"hello": "world"}
|
|
31
|
-
body = json.dumps(payload).encode("utf-8")
|
|
32
|
-
compressed = zstd.ZstdCompressor().compress(body)
|
|
33
|
-
|
|
34
|
-
transport = ASGITransport(app=app)
|
|
35
|
-
async with AsyncClient(transport=transport, base_url="http://testserver") as client:
|
|
36
|
-
resp = await client.post(
|
|
37
|
-
"/echo",
|
|
38
|
-
content=compressed,
|
|
39
|
-
headers={"Content-Encoding": "zstd", "Content-Type": "application/json"},
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
assert resp.status_code == 200
|
|
43
|
-
response_data = resp.json()
|
|
44
|
-
assert response_data["content_encoding"] is None
|
|
45
|
-
assert response_data["data"] == payload
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codex_lb-0.5.0 → codex_lb-0.5.1}/app/db/migrations/versions/add_accounts_chatgpt_account_id.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codex_lb-0.5.0 → codex_lb-0.5.1}/app/db/migrations/versions/add_request_logs_reasoning_effort.py
RENAMED
|
File without changes
|
{codex_lb-0.5.0 → codex_lb-0.5.1}/app/db/migrations/versions/normalize_account_plan_types.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|