Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 38853cdd5a | |||
| f837c0c244 | |||
| 8a7b9cc651 | |||
| 3bf31055e7 | |||
| 3c5bf4fa22 | |||
| 4990ddf9e4 | |||
| de00612965 |
@@ -522,6 +522,7 @@ NALOGO_STORAGE_PATH=./nalogo_tokens.json # Путь к файлу с токен
|
||||
NALOGO_QUEUE_CHECK_INTERVAL=300 # Интервал проверки очереди чеков (секунды)
|
||||
NALOGO_QUEUE_RECEIPT_DELAY=3 # Задержка между отправкой чеков (секунды)
|
||||
NALOGO_QUEUE_MAX_ATTEMPTS=10 # Максимум попыток отправки одного чека
|
||||
# NALOGO_PROXY_URL=socks5://127.0.0.1:1080 # SOCKS прокси для nalog.ru (если не задан — используется PROXY_URL)
|
||||
|
||||
# ===== НАСТРОЙКИ ОПИСАНИЙ ПЛАТЕЖЕЙ =====
|
||||
# Эти настройки позволяют изменить описания платежей,
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "3.37.0"
|
||||
".": "3.38.0"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# Changelog
|
||||
|
||||
## [3.38.0](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/compare/v3.37.0...v3.38.0) (2026-03-21)
|
||||
|
||||
|
||||
### New Features
|
||||
|
||||
* add SOCKS proxy support for nalogo (tax service) module ([3c5bf4f](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/3c5bf4fa22d1cdf144269f4e6ab32a4523c8f1f3))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add diagnostic payload logging in create_user error path ([4990ddf](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/4990ddf9e46495b65fc3638ea8d6bed0cbe6b857))
|
||||
* retry Remnawave API calls without externalSquadUuid on A039 FK violation ([de00612](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/de006129657ce3dac2b1f2fc0ab1b91e23e44241))
|
||||
* sanitize proxy credentials in all nalogo error paths ([3bf3105](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/commit/3bf31055e71ff64e6a6d94486bb7f7775ac7dc91))
|
||||
|
||||
## [3.37.0](https://github.com/BEDOLAGA-DEV/remnawave-bedolaga-telegram-bot/compare/v3.36.1...v3.37.0) (2026-03-21)
|
||||
|
||||
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ RUN --mount=type=cache,target=/root/.cache/uv \
|
||||
|
||||
FROM python:3.13-slim
|
||||
|
||||
ARG VERSION="v3.37.0" # x-release-please-version
|
||||
ARG VERSION="v3.38.0" # x-release-please-version
|
||||
ARG BUILD_DATE
|
||||
ARG VCS_REF
|
||||
|
||||
|
||||
+9
-5
@@ -101,12 +101,16 @@ async def setup_bot() -> tuple[Bot, Dispatcher]:
|
||||
bot = create_bot()
|
||||
|
||||
proxy_url = settings.get_proxy_url()
|
||||
if proxy_url:
|
||||
from urllib.parse import urlparse
|
||||
nalogo_proxy_url = settings.get_nalogo_proxy_url()
|
||||
|
||||
parsed = urlparse(proxy_url)
|
||||
masked = f'{parsed.scheme}://***@{parsed.hostname}:{parsed.port}' if parsed.username else proxy_url
|
||||
logger.info('Proxy configured', proxy_url=masked)
|
||||
if proxy_url or nalogo_proxy_url:
|
||||
from app.utils.proxy import mask_proxy_url
|
||||
|
||||
if proxy_url:
|
||||
logger.info('Proxy configured', proxy_url=mask_proxy_url(proxy_url))
|
||||
if nalogo_proxy_url:
|
||||
source = 'NALOGO_PROXY_URL' if settings.NALOGO_PROXY_URL else 'PROXY_URL (fallback)'
|
||||
logger.info('Nalogo proxy configured', proxy_url=mask_proxy_url(nalogo_proxy_url), source=source)
|
||||
|
||||
maintenance_service.set_bot(bot)
|
||||
logger.info('Бот установлен в maintenance_service')
|
||||
|
||||
+13
-5
@@ -376,6 +376,7 @@ class Settings(BaseSettings):
|
||||
NALOGO_PASSWORD: str | None = None
|
||||
NALOGO_DEVICE_ID: str | None = None
|
||||
NALOGO_STORAGE_PATH: str = './nalogo_tokens.json'
|
||||
NALOGO_PROXY_URL: str | None = None # SOCKS proxy for nalog.ru; falls back to PROXY_URL if not set
|
||||
|
||||
AUTO_PURCHASE_AFTER_TOPUP_ENABLED: bool = False
|
||||
|
||||
@@ -807,7 +808,7 @@ class Settings(BaseSettings):
|
||||
# Format: socks5://user:password@host:port or socks5://host:port
|
||||
PROXY_URL: str | None = None
|
||||
|
||||
@field_validator('PROXY_URL', mode='before')
|
||||
@field_validator('PROXY_URL', 'NALOGO_PROXY_URL', mode='before')
|
||||
@classmethod
|
||||
def validate_proxy_url(cls, value: str | None) -> str | None:
|
||||
if not value:
|
||||
@@ -815,13 +816,13 @@ class Settings(BaseSettings):
|
||||
from urllib.parse import urlparse
|
||||
|
||||
parsed = urlparse(value)
|
||||
if parsed.scheme not in ('socks5', 'socks4'):
|
||||
if parsed.scheme not in ('socks5', 'socks5h', 'socks4'):
|
||||
raise ValueError(
|
||||
f'PROXY_URL must use socks5:// or socks4:// scheme, got: {parsed.scheme!r}. '
|
||||
'HTTP proxies are not supported for security reasons (bot token would be exposed).'
|
||||
f'Proxy URL must use socks5://, socks5h://, or socks4:// scheme, got: {parsed.scheme!r}. '
|
||||
'HTTP proxies are not supported for security reasons.'
|
||||
)
|
||||
if not parsed.hostname:
|
||||
raise ValueError('PROXY_URL must contain a hostname')
|
||||
raise ValueError('Proxy URL must contain a hostname')
|
||||
return value
|
||||
|
||||
@field_validator('MAIN_MENU_MODE', mode='before')
|
||||
@@ -954,6 +955,13 @@ class Settings(BaseSettings):
|
||||
"""Return SOCKS5 proxy URL or None."""
|
||||
return self.PROXY_URL if self.PROXY_URL else None
|
||||
|
||||
def get_nalogo_proxy_url(self) -> str | None:
|
||||
"""Return SOCKS proxy URL for nalogo or None.
|
||||
|
||||
Uses NALOGO_PROXY_URL if set, otherwise falls back to PROXY_URL.
|
||||
"""
|
||||
return self.NALOGO_PROXY_URL or self.PROXY_URL
|
||||
|
||||
def is_admin(self, telegram_id: int | None = None, email: str | None = None) -> bool:
|
||||
"""
|
||||
Check if user is admin by telegram_id or email.
|
||||
|
||||
Vendored
+30
-5
@@ -469,7 +469,22 @@ class RemnaWaveAPI:
|
||||
hwidDeviceLimit=data.get('hwidDeviceLimit'),
|
||||
status=data.get('status'),
|
||||
)
|
||||
response = await self._make_request('POST', '/api/users', data)
|
||||
try:
|
||||
response = await self._make_request('POST', '/api/users', data)
|
||||
except RemnaWaveAPIError as e:
|
||||
# A039 = FK violation on externalSquadUuid — retry without it
|
||||
error_code = (e.response_data or {}).get('errorCode', '')
|
||||
if error_code == 'A039' and 'externalSquadUuid' in data:
|
||||
stale_uuid = data.pop('externalSquadUuid')
|
||||
logger.warning(
|
||||
'A039 FK violation on externalSquadUuid, retrying without it',
|
||||
stale_uuid=stale_uuid,
|
||||
username=data.get('username'),
|
||||
)
|
||||
response = await self._make_request('POST', '/api/users', data)
|
||||
else:
|
||||
logger.error('POST /api/users FAILED — full payload', payload=data)
|
||||
raise
|
||||
user = self._parse_user(response['response'])
|
||||
logger.info(
|
||||
'POST /api/users response',
|
||||
@@ -570,10 +585,20 @@ class RemnaWaveAPI:
|
||||
|
||||
try:
|
||||
response = await self._make_request('PATCH', '/api/users', data)
|
||||
except Exception:
|
||||
# Логируем полный payload при ошибке для диагностики A039
|
||||
logger.error('PATCH /api/users FAILED — full payload', payload=data)
|
||||
raise
|
||||
except RemnaWaveAPIError as e:
|
||||
# A039 = FK violation on externalSquadUuid — retry without it
|
||||
error_code = (e.response_data or {}).get('errorCode', '')
|
||||
if error_code == 'A039' and 'externalSquadUuid' in data:
|
||||
stale_uuid = data.pop('externalSquadUuid')
|
||||
logger.warning(
|
||||
'A039 FK violation on externalSquadUuid, retrying without it',
|
||||
stale_uuid=stale_uuid,
|
||||
uuid=uuid,
|
||||
)
|
||||
response = await self._make_request('PATCH', '/api/users', data)
|
||||
else:
|
||||
logger.error('PATCH /api/users FAILED — full payload', payload=data)
|
||||
raise
|
||||
user = self._parse_user(response['response'])
|
||||
logger.info(
|
||||
'PATCH /api/users response',
|
||||
|
||||
@@ -41,11 +41,13 @@ class AsyncHTTPClient:
|
||||
auth_provider: AuthProvider,
|
||||
default_headers: dict[str, str] | None = None,
|
||||
timeout: float = 10.0,
|
||||
proxy_url: str | None = None,
|
||||
):
|
||||
self.base_url = base_url
|
||||
self.auth_provider = auth_provider
|
||||
self.default_headers = default_headers or {}
|
||||
self.timeout = timeout
|
||||
self.proxy_url = proxy_url
|
||||
self._refresh_lock = asyncio.Lock()
|
||||
self.max_retries = 2 # Same as PHP AuthenticationPlugin::RETRY_LIMIT
|
||||
|
||||
@@ -124,7 +126,7 @@ class AsyncHTTPClient:
|
||||
if json_data is not None:
|
||||
request_kwargs['json'] = json_data
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(proxy=self.proxy_url) as client:
|
||||
# Initial request
|
||||
response = await client.request(**request_kwargs)
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ class AuthProviderImpl(AuthProvider):
|
||||
base_url: str = 'https://lknpd.nalog.ru/api',
|
||||
storage_path: str | None = None,
|
||||
device_id: str | None = None,
|
||||
proxy_url: str | None = None,
|
||||
):
|
||||
self.base_url_v1 = f'{base_url}/v1'
|
||||
self.base_url_v2 = f'{base_url}/v2'
|
||||
@@ -47,6 +48,7 @@ class AuthProviderImpl(AuthProvider):
|
||||
self.device_id = device_id or generate_device_id()
|
||||
self.device_info = DeviceInfo(sourceDeviceId=self.device_id)
|
||||
self._token_data: dict[str, Any] | None = None
|
||||
self.proxy_url = proxy_url
|
||||
|
||||
# Default headers similar to PHP Authenticator
|
||||
self.default_headers = {
|
||||
@@ -130,7 +132,7 @@ class AuthProviderImpl(AuthProvider):
|
||||
'deviceInfo': self.device_info.model_dump(),
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(proxy=self.proxy_url) as client:
|
||||
response = await client.post(
|
||||
f'{self.base_url_v1}/auth/lkfl',
|
||||
json=request_data,
|
||||
@@ -165,7 +167,7 @@ class AuthProviderImpl(AuthProvider):
|
||||
'requireTpToBeActive': True,
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(proxy=self.proxy_url) as client:
|
||||
response = await client.post(
|
||||
f'{self.base_url_v2}/auth/challenge/sms/start',
|
||||
json=request_data,
|
||||
@@ -200,7 +202,7 @@ class AuthProviderImpl(AuthProvider):
|
||||
'deviceInfo': self.device_info.model_dump(),
|
||||
}
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(proxy=self.proxy_url) as client:
|
||||
response = await client.post(
|
||||
f'{self.base_url_v1}/auth/challenge/sms/verify',
|
||||
json=request_data,
|
||||
@@ -233,7 +235,7 @@ class AuthProviderImpl(AuthProvider):
|
||||
}
|
||||
|
||||
try:
|
||||
async with httpx.AsyncClient() as client:
|
||||
async with httpx.AsyncClient(proxy=self.proxy_url) as client:
|
||||
response = await client.post(
|
||||
f'{self.base_url_v1}/auth/token',
|
||||
json=request_data,
|
||||
|
||||
@@ -36,6 +36,7 @@ class Client:
|
||||
storage_path: str | None = None,
|
||||
device_id: str | None = None,
|
||||
timeout: float = 10.0,
|
||||
proxy_url: str | None = None,
|
||||
):
|
||||
"""
|
||||
Initialize Moy Nalog API client.
|
||||
@@ -45,6 +46,7 @@ class Client:
|
||||
storage_path: Optional file path for token storage
|
||||
device_id: Optional device ID (auto-generated if not provided)
|
||||
timeout: HTTP request timeout in seconds
|
||||
proxy_url: Optional SOCKS proxy URL for routing traffic
|
||||
"""
|
||||
self.base_url = base_url
|
||||
self.timeout = timeout
|
||||
@@ -54,6 +56,7 @@ class Client:
|
||||
base_url=base_url,
|
||||
storage_path=storage_path,
|
||||
device_id=device_id,
|
||||
proxy_url=proxy_url,
|
||||
)
|
||||
|
||||
# Initialize HTTP client with auth middleware
|
||||
@@ -67,6 +70,7 @@ class Client:
|
||||
'Referrer': 'https://lknpd.nalog.ru/auth/login',
|
||||
},
|
||||
timeout=timeout,
|
||||
proxy_url=proxy_url,
|
||||
)
|
||||
|
||||
# User profile data (for receipt operations)
|
||||
|
||||
@@ -10,6 +10,7 @@ from app.config import settings
|
||||
from app.lib.nalogo import Client
|
||||
from app.lib.nalogo.dto.income import IncomeClient, IncomeType
|
||||
from app.utils.cache import cache
|
||||
from app.utils.proxy import mask_proxy_url, sanitize_proxy_error
|
||||
|
||||
|
||||
logger = structlog.get_logger(__name__)
|
||||
@@ -41,18 +42,25 @@ class NaloGoService:
|
||||
try:
|
||||
# Таймаут 30 секунд — nalog.ru иногда отвечает медленно
|
||||
timeout = getattr(settings, 'NALOGO_TIMEOUT', 30.0)
|
||||
proxy_url = settings.get_nalogo_proxy_url()
|
||||
self.client = Client(
|
||||
base_url='https://lknpd.nalog.ru/api',
|
||||
storage_path=storage_path,
|
||||
device_id=device_id or 'bot-device-123',
|
||||
timeout=timeout,
|
||||
proxy_url=proxy_url,
|
||||
)
|
||||
self.inn = inn
|
||||
self.password = password
|
||||
self.configured = True
|
||||
logger.info('NaloGO клиент инициализирован для ИНН: ...', inn=inn[:5])
|
||||
if proxy_url:
|
||||
logger.info(
|
||||
'NaloGO клиент инициализирован с прокси', inn=inn[:5], proxy_url=mask_proxy_url(proxy_url)
|
||||
)
|
||||
else:
|
||||
logger.info('NaloGO клиент инициализирован для ИНН: ...', inn=inn[:5])
|
||||
except Exception as error:
|
||||
logger.error('Ошибка инициализации NaloGO клиента', error=error, exc_info=True)
|
||||
logger.error('Ошибка инициализации NaloGO клиента', error=sanitize_proxy_error(error))
|
||||
self.configured = False
|
||||
|
||||
@staticmethod
|
||||
@@ -281,9 +289,9 @@ class NaloGoService:
|
||||
return True
|
||||
except Exception as error:
|
||||
if self._is_service_unavailable(error):
|
||||
logger.warning('NaloGO временно недоступен (техработы)', error=str(error)[:200])
|
||||
logger.warning('NaloGO временно недоступен (техработы)', error=sanitize_proxy_error(error))
|
||||
else:
|
||||
logger.error('Ошибка аутентификации в NaloGO', error=error, exc_info=True)
|
||||
logger.error('Ошибка аутентификации в NaloGO', error=sanitize_proxy_error(error))
|
||||
return False
|
||||
|
||||
async def create_receipt(
|
||||
@@ -355,7 +363,7 @@ class NaloGoService:
|
||||
name, amount, quantity, client_info, payment_id, telegram_user_id, amount_kopeks
|
||||
)
|
||||
else:
|
||||
logger.error('Ошибка аутентификации NaloGO', auth_error=auth_error, exc_info=True)
|
||||
logger.error('Ошибка аутентификации NaloGO', auth_error=sanitize_proxy_error(auth_error))
|
||||
return None
|
||||
|
||||
# ЭТАП 2: Создание чека
|
||||
@@ -400,9 +408,9 @@ class NaloGoService:
|
||||
# ВАЖНО: Аутентификация была успешной, запрос на создание чека УШЁЛ
|
||||
# При таймауте чек МОГ быть создан на сервере — НЕ добавляем в очередь!
|
||||
if self._is_service_unavailable(error):
|
||||
error_msg = str(error)[:200]
|
||||
error_msg = sanitize_proxy_error(error)[:200]
|
||||
logger.error(
|
||||
'⚠️ ТАЙМАУТ после успешной аутентификации! Чек МОГ быть создан! (payment_id=, сумма=₽). Сохраняем в очередь проверки. Проверьте lknpd.nalog.ru',
|
||||
'ТАЙМАУТ после успешной аутентификации! Чек МОГ быть создан!',
|
||||
payment_id=payment_id,
|
||||
amount=amount,
|
||||
)
|
||||
@@ -418,7 +426,7 @@ class NaloGoService:
|
||||
error_message=error_msg,
|
||||
)
|
||||
else:
|
||||
logger.error('Ошибка создания чека в NaloGO', error=error, exc_info=True)
|
||||
logger.error('Ошибка создания чека в NaloGO', error=sanitize_proxy_error(error))
|
||||
return None
|
||||
|
||||
async def get_queue_length(self) -> int:
|
||||
@@ -511,7 +519,7 @@ class NaloGoService:
|
||||
return None
|
||||
|
||||
except Exception as error:
|
||||
logger.warning('Ошибка проверки дубликата чека', error=error)
|
||||
logger.warning('Ошибка проверки дубликата чека', error=sanitize_proxy_error(error))
|
||||
return None
|
||||
|
||||
async def get_incomes(
|
||||
@@ -555,7 +563,7 @@ class NaloGoService:
|
||||
|
||||
except Exception as error:
|
||||
if self._is_service_unavailable(error):
|
||||
logger.warning('NaloGO временно недоступен', error=error)
|
||||
logger.warning('NaloGO временно недоступен', error=sanitize_proxy_error(error))
|
||||
else:
|
||||
logger.error('Ошибка получения списка доходов', error=error, exc_info=True)
|
||||
logger.error('Ошибка получения списка доходов', error=sanitize_proxy_error(error))
|
||||
return None # None = ошибка, [] = нет чеков
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Proxy URL utilities for safe logging and error handling."""
|
||||
|
||||
import re
|
||||
from urllib.parse import urlparse
|
||||
|
||||
|
||||
def mask_proxy_url(proxy_url: str) -> str:
|
||||
"""Mask credentials in a proxy URL for safe logging.
|
||||
|
||||
Handles edge cases:
|
||||
- No credentials: returns URL as-is
|
||||
- Username + password: masks both with ***
|
||||
- Password-only: masks as well
|
||||
- No explicit port: omits :port part
|
||||
"""
|
||||
parsed = urlparse(proxy_url)
|
||||
if not parsed.username and not parsed.password:
|
||||
return proxy_url
|
||||
host = parsed.hostname or 'unknown'
|
||||
port_part = f':{parsed.port}' if parsed.port else ''
|
||||
return f'{parsed.scheme}://***@{host}{port_part}'
|
||||
|
||||
|
||||
_PROXY_CRED_RE = re.compile(r'(socks[45h]*://)([^@\s]+@)', re.IGNORECASE)
|
||||
|
||||
|
||||
def sanitize_proxy_error(error: Exception) -> str:
|
||||
"""Strip proxy credentials from exception messages.
|
||||
|
||||
httpx/socksio may include the full proxy URL (with credentials)
|
||||
in connection error messages and tracebacks. This function removes
|
||||
credentials from the error string while preserving the original scheme.
|
||||
"""
|
||||
msg = str(error)
|
||||
return _PROXY_CRED_RE.sub(r'\1***@', msg)
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = 'remnawave-bedolaga-telegram-bot'
|
||||
version = "3.37.0"
|
||||
version = "3.38.0"
|
||||
description = 'Telegram bot for RemnaWave VPN service'
|
||||
readme = 'README.md'
|
||||
license = { text = 'MIT' }
|
||||
@@ -25,6 +25,7 @@ dependencies = [
|
||||
'structlog>=25.1.0,<26',
|
||||
'rich>=14.0',
|
||||
'aiohttp-socks>=0.10.1',
|
||||
'httpx[socks]>=0.27.0',
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
||||
@@ -623,6 +623,11 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
socks = [
|
||||
{ name = "socksio" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
@@ -1137,7 +1142,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "remnawave-bedolaga-telegram-bot"
|
||||
version = "3.36.0"
|
||||
version = "3.36.1"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "aiogram" },
|
||||
@@ -1148,6 +1153,7 @@ dependencies = [
|
||||
{ name = "bcrypt" },
|
||||
{ name = "cryptography" },
|
||||
{ name = "fastapi", extra = ["standard"] },
|
||||
{ name = "httpx", extra = ["socks"] },
|
||||
{ name = "packaging" },
|
||||
{ name = "pyjwt" },
|
||||
{ name = "python-dateutil" },
|
||||
@@ -1179,6 +1185,7 @@ requires-dist = [
|
||||
{ name = "bcrypt", specifier = ">=5.0.0" },
|
||||
{ name = "cryptography", specifier = ">=44.0.1" },
|
||||
{ name = "fastapi", extras = ["standard"], specifier = ">=0.129.0" },
|
||||
{ name = "httpx", extras = ["socks"], specifier = ">=0.27.0" },
|
||||
{ name = "packaging", specifier = ">=26.0" },
|
||||
{ name = "pyjwt", specifier = ">=2.11.0" },
|
||||
{ name = "python-dateutil", specifier = ">=2.9.0.post0" },
|
||||
@@ -1322,6 +1329,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socksio"
|
||||
version = "1.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac", size = 19055, upload-time = "2020-04-17T15:50:34.664Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763, upload-time = "2020-04-17T15:50:31.878Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sqlalchemy"
|
||||
version = "2.0.46"
|
||||
|
||||
Reference in New Issue
Block a user