refactor: remove Flask, use FastAPI exclusively for all webhooks

Delete dead Flask-based PAL24 webhook server (app/external/pal24_webhook.py).
PAL24 webhooks already handled by unified FastAPI server on port 8080.

- Remove flask dependency from pyproject.toml and requirements.txt
- Remove PAL24_WEBHOOK_PORT config (unused, FastAPI uses shared port)
- Remove pal24_webhook module reference from log filter
- Update docs: webhook example rewritten from Flask to FastAPI
- Uninstall flask, werkzeug, blinker, itsdangerous
This commit is contained in:
Fringg
2026-02-09 21:54:15 +03:00
parent a3903a252e
commit 119f463c36
9 changed files with 10 additions and 235 deletions
-1
View File
@@ -544,7 +544,6 @@ PAL24_SHOP_ID=
PAL24_SIGNATURE_TOKEN=
PAL24_BASE_URL=https://pal24.pro/api/v1/
PAL24_WEBHOOK_PATH=/pal24-webhook
PAL24_WEBHOOK_PORT=8084
PAL24_PAYMENT_DESCRIPTION="Пополнение баланса"
PAL24_MIN_AMOUNT_KOPEKS=10000
PAL24_MAX_AMOUNT_KOPEKS=100000000
-1
View File
@@ -403,7 +403,6 @@ class Settings(BaseSettings):
PAL24_SIGNATURE_TOKEN: str | None = None
PAL24_BASE_URL: str = 'https://pal24.pro/api/v1/'
PAL24_WEBHOOK_PATH: str = '/pal24-webhook'
PAL24_WEBHOOK_PORT: int = 8084
PAL24_PAYMENT_DESCRIPTION: str = 'Пополнение баланса'
PAL24_MIN_AMOUNT_KOPEKS: int = 10000
PAL24_MAX_AMOUNT_KOPEKS: int = 100000000
-166
View File
@@ -1,166 +0,0 @@
"""Flask webhook server for PayPalych callbacks."""
from __future__ import annotations
import asyncio
import json
import logging
import threading
from asyncio import AbstractEventLoop
from concurrent.futures import TimeoutError as FuturesTimeoutError
from typing import Any
from flask import Flask, jsonify, request
from werkzeug.serving import make_server
from app.config import settings
from app.database.database import AsyncSessionLocal
from app.services.pal24_service import Pal24APIError, Pal24Service
from app.services.payment_service import PaymentService
logger = logging.getLogger(__name__)
def _normalize_payload() -> dict[str, str]:
if request.is_json:
payload = request.get_json(silent=True) or {}
if isinstance(payload, dict):
return {k: str(v) for k, v in payload.items()}
logger.warning('Pal24 webhook JSON payload не является объектом: %s', payload)
return {}
if request.form:
return {k: v for k, v in request.form.items()}
try:
raw_body = request.data.decode('utf-8')
if raw_body:
payload = json.loads(raw_body)
if isinstance(payload, dict):
return {k: str(v) for k, v in payload.items()}
except json.JSONDecodeError:
logger.debug('Pal24 webhook body не удалось распарсить как JSON')
return {}
def create_pal24_flask_app(
payment_service: PaymentService,
loop: AbstractEventLoop,
) -> Flask:
pal24_service = Pal24Service()
app = Flask(__name__)
@app.route(settings.PAL24_WEBHOOK_PATH, methods=['POST'])
def pal24_webhook() -> tuple:
if not pal24_service.is_configured:
logger.error('Pal24 webhook получен, но сервис не настроен')
return jsonify({'status': 'error', 'reason': 'service_not_configured'}), 503
logger.debug('Получен Pal24 webhook: headers=%s', dict(request.headers))
payload = _normalize_payload()
if not payload:
logger.warning('Пустой Pal24 webhook')
return jsonify({'status': 'error', 'reason': 'empty_payload'}), 400
try:
parsed_payload = pal24_service.parse_callback(payload)
except Pal24APIError as error:
logger.error('Ошибка валидации Pal24 webhook: %s', error)
return jsonify({'status': 'error', 'reason': str(error)}), 400
async def process() -> bool:
async with AsyncSessionLocal() as db:
try:
return await payment_service.process_pal24_callback(db, parsed_payload)
except Exception:
await db.rollback()
raise
try:
future = asyncio.run_coroutine_threadsafe(process(), loop)
processed = future.result(timeout=settings.PAL24_REQUEST_TIMEOUT)
except FuturesTimeoutError:
logger.error('Обработка Pal24 webhook превысила таймаут %sс', settings.PAL24_REQUEST_TIMEOUT)
return jsonify({'status': 'error', 'reason': 'timeout'}), 504
except Exception as error: # pragma: no cover - defensive
logger.exception('Критическая ошибка обработки Pal24 webhook: %s', error)
return jsonify({'status': 'error', 'reason': 'internal_error'}), 500
if processed:
return jsonify({'status': 'ok'}), 200
return jsonify({'status': 'error', 'reason': 'not_processed'}), 400
@app.route(settings.PAL24_WEBHOOK_PATH, methods=['GET'])
def pal24_health() -> tuple:
return jsonify(
{
'status': 'ok',
'service': 'pal24_webhook',
'enabled': settings.is_pal24_enabled(),
}
), 200
@app.route('/pal24/health', methods=['GET'])
def pal24_additional_health() -> tuple:
return jsonify(
{
'status': 'ok',
'service': 'pal24_webhook',
'path': settings.PAL24_WEBHOOK_PATH,
}
), 200
return app
class Pal24WebhookServer:
"""Threaded Flask server for Pal24 callbacks."""
def __init__(self, payment_service: PaymentService, loop: AbstractEventLoop) -> None:
self.app = create_pal24_flask_app(payment_service, loop)
self._server: Any | None = None
self._thread: threading.Thread | None = None
def start(self) -> None:
if self._server:
logger.warning('Pal24 webhook server уже запущен')
return
self._server = make_server(
host='0.0.0.0',
port=settings.PAL24_WEBHOOK_PORT,
app=self.app,
threaded=True,
)
def _serve() -> None:
logger.info(
'Pal24 webhook сервер запущен на %s:%s%s',
'0.0.0.0',
settings.PAL24_WEBHOOK_PORT,
settings.PAL24_WEBHOOK_PATH,
)
self._server.serve_forever()
self._thread = threading.Thread(target=_serve, daemon=True)
self._thread.start()
def stop(self) -> None:
if self._server:
logger.info('Останавливаем Pal24 webhook сервер')
self._server.shutdown()
self._server = None
if self._thread and self._thread.is_alive():
self._thread.join(timeout=5)
self._thread = None
async def start_pal24_webhook_server(payment_service: PaymentService) -> Pal24WebhookServer:
loop = asyncio.get_running_loop()
server = Pal24WebhookServer(payment_service, loop)
await loop.run_in_executor(None, server.start)
return server
-1
View File
@@ -81,7 +81,6 @@ class PaymentLogFilter(logging.Filter):
'app.external.heleket',
'app.external.tribute',
'app.external.yookassa_webhook',
'app.external.pal24_webhook',
'app.external.wata_webhook',
'app.external.heleket_webhook',
)
-3
View File
@@ -170,9 +170,6 @@
- `app/external/pal24_client.py` — Async client for PayPalych (Pal24) API.
Классы: `Pal24APIError` — Base error for Pal24 API operations., `Pal24Response` (2 методов) — Wrapper for Pal24 API responses., `Pal24Client` (5 методов) — Async client implementing PayPalych API methods.
Функции: нет
- `app/external/pal24_webhook.py` — Flask webhook server for PayPalych callbacks.
Классы: `Pal24WebhookServer` (3 методов) — Threaded Flask server for Pal24 callbacks.
Функции: `_normalize_payload`, `create_pal24_flask_app`
- `app/external/remnawave_api.py` — Python-модуль
Классы: `UserStatus`, `TrafficLimitStrategy`, `RemnaWaveUser`, `RemnaWaveInternalSquad`, `RemnaWaveNode`, `SubscriptionInfo`, `RemnaWaveAPIError` (1 методов), `RemnaWaveAPI` (8 методов)
Функции: `format_bytes`, `parse_bytes`
+10 -10
View File
@@ -254,31 +254,31 @@ setInterval(() => {
### Python Webhook receiver
```python
from flask import Flask, request
from fastapi import FastAPI, Request, HTTPException
import hmac
import hashlib
import json
app = Flask(__name__)
app = FastAPI()
WEBHOOK_SECRET = "your-secret"
@app.route('/webhook', methods=['POST'])
def webhook():
@app.post('/webhook')
async def webhook(request: Request):
signature = request.headers.get('X-Webhook-Signature', '')
event_type = request.headers.get('X-Webhook-Event')
payload = request.json
payload = await request.json()
# Проверка подписи
if not verify_signature(payload, signature, WEBHOOK_SECRET):
return {'error': 'Invalid signature'}, 401
raise HTTPException(status_code=401, detail='Invalid signature')
# Обработка события
if event_type == 'user.created':
handle_new_user(payload)
elif event_type == 'payment.completed':
handle_payment(payload)
return {'status': 'ok'}, 200
return {'status': 'ok'}
def verify_signature(payload, signature, secret):
payload_json = json.dumps(payload, sort_keys=True)
-1
View File
@@ -21,7 +21,6 @@ dependencies = [
'packaging>=23.2',
'bcrypt>=4.2.0',
'pyjwt>=2.8.0',
'flask>=3.1.0',
'pyzipper>=0.3.6',
]
-3
View File
@@ -46,8 +46,5 @@ packaging==23.2
aiofiles==23.2.1
# Вебхуки PayPalych (Flask)
Flask==3.1.0
# Архивирование с паролем
pyzipper==0.3.6
Generated
-49
View File
@@ -214,15 +214,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" },
]
[[package]]
name = "blinker"
version = "1.9.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/28/9b3f50ce0e048515135495f198351908d99540d69bfdc8c1d15b73dc55ce/blinker-1.9.0.tar.gz", hash = "sha256:b4ce2265a7abece45e7cc896e98dbebe6cead56bcf805a3d23136d145f5445bf", size = 22460, upload-time = "2024-11-08T17:25:47.436Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/cb/f2ad4230dc2eb1a74edf38f1a38b9b52277f75bef262d8908e60d957e13c/blinker-1.9.0-py3-none-any.whl", hash = "sha256:ba0efaa9080b619ff2f3459d1d500c57bddea4a6b424b60a91141db6fd2f08bc", size = 8458, upload-time = "2024-11-08T17:25:46.184Z" },
]
[[package]]
name = "certifi"
version = "2026.1.4"
@@ -509,23 +500,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/dd/10/c99202719b83e5249f26902ae53a05aea67d840eeb242019322f20fc171c/fastar-0.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:c4c4bd08df563120cd33e854fe0a93b81579e8571b11f9b7da9e84c37da2d6b6", size = 461078, upload-time = "2025-11-26T02:36:04.94Z" },
]
[[package]]
name = "flask"
version = "3.1.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "blinker" },
{ name = "click" },
{ name = "itsdangerous" },
{ name = "jinja2" },
{ name = "markupsafe" },
{ name = "werkzeug" },
]
sdist = { url = "https://files.pythonhosted.org/packages/dc/6d/cfe3c0fcc5e477df242b98bfe186a4c34357b4847e87ecaef04507332dab/flask-3.1.2.tar.gz", hash = "sha256:bf656c15c80190ed628ad08cdfd3aaa35beb087855e2f494910aa3774cc4fd87", size = 720160, upload-time = "2025-08-19T21:03:21.205Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/f9/7f9263c5695f4bd0023734af91bedb2ff8209e8de6ead162f35d8dc762fd/flask-3.1.2-py3-none-any.whl", hash = "sha256:ca1d8112ec8a6158cc29ea4858963350011b5c846a414cdb7a954aa9e967d03c", size = 103308, upload-time = "2025-08-19T21:03:19.499Z" },
]
[[package]]
name = "frozenlist"
version = "1.8.0"
@@ -653,15 +627,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
]
[[package]]
name = "itsdangerous"
version = "2.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9c/cb/8ac0172223afbccb63986cc25049b154ecfb5e85932587206f42317be31d/itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173", size = 54410, upload-time = "2024-04-16T21:28:15.614Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/96/92447566d16df59b2a776c0fb82dbc4d9e07cd95062562af01e408583fc4/itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef", size = 16234, upload-time = "2024-04-16T21:28:14.499Z" },
]
[[package]]
name = "jinja2"
version = "3.1.6"
@@ -1159,7 +1124,6 @@ dependencies = [
{ name = "bcrypt" },
{ name = "cryptography" },
{ name = "fastapi", extra = ["standard"] },
{ name = "flask" },
{ name = "packaging" },
{ name = "pyjwt" },
{ name = "python-dateutil" },
@@ -1188,7 +1152,6 @@ requires-dist = [
{ name = "bcrypt", specifier = ">=4.2.0" },
{ name = "cryptography", specifier = ">=41.0.0" },
{ name = "fastapi", extras = ["standard"], specifier = ">=0.115.6" },
{ name = "flask", specifier = ">=3.1.0" },
{ name = "packaging", specifier = ">=23.2" },
{ name = "pyjwt", specifier = ">=2.8.0" },
{ name = "python-dateutil", specifier = ">=2.9.0.post0" },
@@ -1501,18 +1464,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
]
[[package]]
name = "werkzeug"
version = "3.1.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" },
]
[[package]]
name = "wrapt"
version = "2.0.1"