release 1.2

This commit is contained in:
2026-03-03 22:43:26 +03:00
parent 651631b3c7
commit 40dec27d41
27 changed files with 494 additions and 496 deletions
+6
View File
@@ -0,0 +1,6 @@
compose.yml
apiKey.csv
Dockerfile
.gitignore
.DS_Store
.env
+4
View File
@@ -1,4 +1,8 @@
# ---> Python # ---> Python
/attachments
.DS_Store
# Byte-compiled / optimized / DLL files # Byte-compiled / optimized / DLL files
__pycache__/ __pycache__/
*.py[cod] *.py[cod]
+33
View File
@@ -0,0 +1,33 @@
FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim
WORKDIR /app
# Install all WeasyPrint dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
python3-dev \
python3-pip \
python3-setuptools \
python3-wheel \
python3-cffi \
libcairo2 \
libpango-1.0-0 \
libpangocairo-1.0-0 \
libgdk-pixbuf-2.0-0 \
libffi-dev \
shared-mime-info \
libpangoft2-1.0-0 \
libharfbuzz0b \
libthai0 \
fonts-dejavu \
fonts-liberation \
fonts-freefont-ttf \
&& rm -rf /var/lib/apt/lists/*
COPY . .
RUN uv sync --frozen
RUN chmod +x /app/start.sh
CMD ["/app/start.sh"]
+47
View File
@@ -0,0 +1,47 @@
# compose.yml
services:
postgresql:
container_name: postgresql
environment:
POSTGRES_DB: dolgolet
POSTGRES_PASSWORD: dolgolet
POSTGRES_USER: dolgolet
TZ: Europe/Moscow
healthcheck:
test: ["CMD-SHELL", "pg_isready -U dolgolet -d dolgolet"]
timeout: 10s
interval: 15s
retries: 7
start_period: 12s
image: postgres:18
networks:
- my-net
restart: always
volumes:
- /mnt/dbdata/postgres:/var/lib/postgresql
medodston3health:
depends_on:
postgresql:
condition: service_healthy
image: medodston3health:latest
container_name: medodston3health
volumes:
- /mnt/dbdata/attachments:/app/src/attachments
restart: unless-stopped
ports:
- "80:80"
networks:
- my-net
environment:
TZ: Europe/Moscow
DB_HOST: postgresql
DB_PORT: 5432
DB_USER: dolgolet
DB_PASSWORD: dolgolet
DB_NAME: dolgolet
networks:
my-net:
external: true
+3 -1
View File
@@ -13,6 +13,7 @@ dependencies = [
"colorlog>=6.10.1", "colorlog>=6.10.1",
"fastapi>=0.128.0", "fastapi>=0.128.0",
"greenlet>=3.3.1", "greenlet>=3.3.1",
"gunicorn>=25.1.0",
"jinja2>=3.1.6", "jinja2>=3.1.6",
"lxml>=6.0.2", "lxml>=6.0.2",
"pydantic>=2.12.5", "pydantic>=2.12.5",
@@ -20,5 +21,6 @@ dependencies = [
"python-dotenv>=1.2.1", "python-dotenv>=1.2.1",
"python-multipart>=0.0.21", "python-multipart>=0.0.21",
"sqlalchemy>=2.0.46", "sqlalchemy>=2.0.46",
"uvicorn>=0.40.0", "uvicorn>=0.41.0",
"weasyprint>=68.1",
] ]
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -196,7 +196,7 @@ async def getTrackingIDSigning(trackingID: str, singingId: int):
if not content: if not content:
continue continue
ATTACHMENTS_DIR = Path("src/attachments/import") ATTACHMENTS_DIR = Path("attachments/import").resolve()
result = detect_and_save_attachment( result = detect_and_save_attachment(
b64_content=content, b64_content=content,
output_dir=ATTACHMENTS_DIR / str(singingId), output_dir=ATTACHMENTS_DIR / str(singingId),
+2 -2
View File
@@ -524,8 +524,8 @@ async def search_recipients_post(
settings = await Settings.getSettings(False) settings = await Settings.getSettings(False)
if not settings.success: if not settings.success:
logger.error(settings.error) logger.error(settings.message)
return {"status": "error", "message": settings.error} return {"status": "error", "message": settings.message}
settings = settings.data settings = settings.data
try: try:
patientsDB = await findPatientByPhone( patientsDB = await findPatientByPhone(
-1
View File
@@ -17,7 +17,6 @@ async def settingsPage(request: Request):
@router.get("/get", name="getSettings", summary="Получить настройки") @router.get("/get", name="getSettings", summary="Получить настройки")
async def getSettings(): async def getSettings():
settings = await Settings.getSettings() settings = await Settings.getSettings()
# logger.info(settings.data)
return settings.data return settings.data
+7 -2
View File
@@ -364,7 +364,12 @@ function updateStaffTable() {
// Определяем тип подписи // Определяем тип подписи
let signatureTypeHtml; let signatureTypeHtml;
if (practitioner.esiaAuth) { if (practitioner.esiaAuth) {
signatureTypeHtml = '<span class="badge bg-success">УКЭП</span>'; const snils = (practitioner.snils || '').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
const snilsIcon = `<button type="button" class="btn btn-sm btn-link p-0 ms-1"
onclick="showAttorneyPopup('${snils}', this)">
<i class="bi bi-eye"></i>
</button>`;
signatureTypeHtml = `<span class="badge bg-success me-4">УКЭП</span><span class="badge bg-info">СНИЛС</span>${snilsIcon}`;
} else { } else {
// Экранируем специальные символы в attorney // Экранируем специальные символы в attorney
const attorney = (practitioner.attorney || '').replace(/"/g, '&quot;').replace(/'/g, '&#39;'); const attorney = (practitioner.attorney || '').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
@@ -914,7 +919,7 @@ document.addEventListener('DOMContentLoaded', function () {
const userIdLpu = document.getElementById('ukepPractitionerId').value; const userIdLpu = document.getElementById('ukepPractitionerId').value;
const ukepFile = document.getElementById('ukepFile').files[0]; const ukepFile = document.getElementById('ukepFile').files[0];
const expiryDate = document.getElementById('ukepExpiryDate').value; const expiryDate = document.getElementById('ukepExpiryDate').value;
const snils = document.getElementById('mchdSnilsNumber').value.replace(/\D/g, ''); const snils = document.getElementById('ukepSnilsNumber').value.replace(/\D/g, '');
if (!expiryDate) { if (!expiryDate) {
showAlert('Заполните дату окончания действия', 'warning'); showAlert('Заполните дату окончания действия', 'warning');
-21
View File
@@ -1,21 +0,0 @@
{% extends "base.html" %}
<!-- Заголовок страницы -->
{% block title %}
{% endblock %}
<!-- Блок шапки -->
{% block head %}
{% endblock %}
<!-- Блок тела -->
{% block body %}
{% endblock %}
<!-- Блок скриптов -->
{% block scripts %}
{% endblock %}
+1 -1
View File
@@ -236,7 +236,7 @@
<div id="attorneyPopup" class="position-fixed bg-white border rounded shadow p-3" <div id="attorneyPopup" class="position-fixed bg-white border rounded shadow p-3"
style="display: none; z-index: 1060; max-width: 400px;"> style="display: none; z-index: 1060; max-width: 400px;">
<div class="d-flex justify-content-between align-items-start mb-2"> <div class="d-flex justify-content-between align-items-start mb-2">
<h6 class="mb-0">Номер МЧД</h6> <h6 class="mb-0">Номер:</h6>
<button type="button" class="btn-close btn-sm" id="closeAttorneyPopupBtn"></button> <button type="button" class="btn-close btn-sm" id="closeAttorneyPopupBtn"></button>
</div> </div>
<div class="mb-2"> <div class="mb-2">
BIN
View File
Binary file not shown.
Binary file not shown.
-87
View File
@@ -1,87 +0,0 @@
{
"201": {
"ru": "Создан",
"en": "Created",
"description": "Документ создан через API, готов к обработке"
},
"202": {
"ru": "Отправлен на подписание",
"en": "Sent for Signing",
"description": "Документ отправлен получателю"
},
"203": {
"ru": "Просмотрел",
"en": "Viewed",
"description": "Документ просмотрен получателем"
},
"204": {
"ru": "Подписал",
"en": "Signed",
"description": "Документ подписан получателем"
},
"205": {
"ru": "Отказался",
"en": "Rejected",
"description": "Получатель отказался от подписания"
},
"206": {
"ru": "Срок для подписания истек",
"en": "Signing Period Expired",
"description": "Срок действия ссылки истек до момента проставления подписи или отказа"
},
"207": {
"ru": "Ожидание действий всех получателей",
"en": "Awaiting Actions from All Recipients",
"description": "Ожидание действий от всех получателей"
},
"208": {
"ru": "Успешно. Подписан всеми — обработка",
"en": "Success. Fully Signed — Processing",
"description": "Все получатели подписали, документ обрабатывается"
},
"209": {
"ru": "Успешно. Подписан не всеми — обработка",
"en": "Success. Partially Signed — Processing",
"description": "Не все получатели подписали, документ обрабатывается"
},
"210": {
"ru": "Завершено. Подписано всеми",
"en": "Completed. Fully Signed",
"description": "Все получатели подписали, документ сохранен"
},
"211": {
"ru": "Завершено. Подписано не всеми",
"en": "Completed. Partially Signed",
"description": "Не все получатели подписали, документ сохранен"
},
"212": {
"ru": "Отменен для получателя",
"en": "Cancelled for Recipient",
"description": "Подписание отменено для конкретного получателя"
},
"213": {
"ru": "Завершено. Отменено для всех",
"en": "Completed. Cancelled for All",
"description": "Подписание отменено для всех получателей"
},
"498": {
"ru": "Завершено. Отклонено всеми",
"en": "Completed. Rejected by All",
"description": "Все получатели отказались от подписания"
},
"499": {
"ru": "Завершено. Истекло для всех",
"en": "Completed. Expired for All",
"description": "Срок ссылки истек для всех получателей"
},
"500": {
"ru": "Ошибка обработки",
"en": "Processing Error",
"description": "Ошибка во время обработки документа"
},
"501": {
"ru": "Ошибка доставки получателю",
"en": "Delivery Error",
"description": "Произошла ошибка при доставке конкретному получателю"
}
}
+1
View File
@@ -43,6 +43,7 @@ class DatabaseInitializer:
if not tables_exist: if not tables_exist:
logger.warning("Не все таблицы существуют. Создаем недостающие...") logger.warning("Не все таблицы существуют. Создаем недостающие...")
await self._create_tables_directly() await self._create_tables_directly()
await self._initialize_data()
# 🔥 СИНХРОНИЗАЦИЯ СХЕМЫ # 🔥 СИНХРОНИЗАЦИЯ СХЕМЫ
logger.info("Синхронизация схемы БД...") logger.info("Синхронизация схемы БД...")
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+2 -21
View File
@@ -5,27 +5,14 @@ from contextlib import asynccontextmanager
from app.routers import router from app.routers import router
import config import config
from utils import logger
from utils import logger
from utils.background import init_scheduler, scheduler_lifespan from utils.background import init_scheduler, scheduler_lifespan
# Инициализация задач планировщика (без запуска) # Инициализация задач планировщика (без запуска)
init_scheduler() init_scheduler()
async def initDB():
from db import DATABASE_URL
from db.initialize import DatabaseInitializer
try:
force = False
reNewDB = False
await DatabaseInitializer(DATABASE_URL).initialize(force, reNewDB)
logger.info("База данных инициализирована")
except Exception as e:
logger.error(f"Инициализация базы завершилась ошибкой: {str(e)}", exc_info=True)
@asynccontextmanager @asynccontextmanager
async def lifespan(app: FastAPI): async def lifespan(app: FastAPI):
# Запускаем lifespan планировщика # Запускаем lifespan планировщика
@@ -89,13 +76,7 @@ def startServer():
) )
async def main():
startServer()
if __name__ == "__main__": if __name__ == "__main__":
import asyncio import asyncio
asyncio.run(initDB()) asyncio.run(startServer())
asyncio.run(main())
+18
View File
@@ -0,0 +1,18 @@
async def initDB():
from utils import logger
from db import DATABASE_URL
from db.initialize import DatabaseInitializer
try:
force = False
reNewDB = False
await DatabaseInitializer(DATABASE_URL).initialize(force, reNewDB)
logger.info("База данных инициализирована")
except Exception as e:
logger.error(f"Инициализация базы завершилась ошибкой: {str(e)}", exc_info=True)
if __name__ == "__main__":
import asyncio
asyncio.run(initDB())
+2 -2
View File
@@ -92,7 +92,7 @@ def p7s_save(signature_data, employee_id):
try: try:
# Базовый путь к директории # Базовый путь к директории
base_path = Path("src/attachments/secure") base_path = Path("attachments/secure").resolve()
# Путь к папке сотрудника # Путь к папке сотрудника
employee_path = base_path / str(employee_id) employee_path = base_path / str(employee_id)
@@ -141,7 +141,7 @@ def p7s_delete(employee_id):
from utils import answer from utils import answer
try: try:
signature_dir = Path(f"src/attachments/secure/{employee_id}") signature_dir = Path(f"attachments/secure/{employee_id}").resolve()
if signature_dir.exists(): if signature_dir.exists():
for file in signature_dir.iterdir(): for file in signature_dir.iterdir():
+83 -352
View File
@@ -1,70 +1,47 @@
"""
Модуль для конвертации HTML в PDF с использованием системного WeasyPrint (Homebrew)
Фильтрация пустых строк таблиц, центрирование по text-align: center.
"""
import os
import re import re
import subprocess
import tempfile
from typing import List, Dict, Tuple from typing import List, Dict, Tuple
from pathlib import Path from pathlib import Path
from weasyprint import HTML
from utils import logger from utils import logger
class WeasyPrintConverter: class WeasyPrintConverter:
def __init__(self, output_dir: list[str]): def __init__(self, output_dir: str):
self.output_dir = Path(output_dir).resolve() self.output_dir = Path(output_dir).resolve()
self.output_dir.mkdir(parents=True, exist_ok=True) self.output_dir.mkdir(parents=True, exist_ok=True)
self.max_files = 5 self.max_files = 5
self.weasyprint_path = self._find_weasyprint() # ---------------------------------------------------------
# SERVICE METHODS
def _find_weasyprint(self): # ---------------------------------------------------------
try:
result = subprocess.run(
["which", "weasyprint"], capture_output=True, text=True, check=True
)
weasyprint_path = result.stdout.strip()
logger.info(f"✅ WeasyPrint найден: {weasyprint_path}")
return weasyprint_path
except subprocess.CalledProcessError:
possible_paths = [
"/opt/homebrew/bin/weasyprint",
"/usr/local/bin/weasyprint",
"/usr/bin/weasyprint",
]
for path in possible_paths:
if os.path.exists(path):
logger.info(f"✅ WeasyPrint найден: {path}")
return path
logger.error("❌ WeasyPrint не найден. Установите: brew install weasyprint")
return None
def _clean_filename(self, filename: str) -> str: def _clean_filename(self, filename: str) -> str:
"""Очищает имя файла от недопустимых символов"""
# Только заменяем запрещённые символы на подчёркивание
clean_name = re.sub(r'[<>:"/\\|?*]', "_", filename) clean_name = re.sub(r'[<>:"/\\|?*]', "_", filename)
# Убираем пробелы по краям
clean_name = clean_name.strip() clean_name = clean_name.strip()
# Ограничиваем длину
if len(clean_name) > 200: if len(clean_name) > 200:
clean_name = clean_name[:200] clean_name = clean_name[:200]
# Защита от пустого имени
return clean_name if clean_name else "document" return clean_name if clean_name else "document"
# ---------------------------------------------------------
# TABLE CLEANING
# ---------------------------------------------------------
def _is_empty_table_row(self, row_html: str) -> bool:
text_only = re.sub(r"<[^>]+>", "", row_html)
clean_text = re.sub(r"[\s\xa0\u200b\u200c\u200d\u2060\ufeff]+", "", text_only)
clean_text = re.sub(r"&[^;]{2,6};", "", clean_text)
has_colspan = "colspan" in row_html.lower()
has_rowspan = "rowspan" in row_html.lower()
has_nbsp = "&nbsp;" in row_html or "&#160;" in row_html
is_not_empty = bool(clean_text) or has_colspan or has_rowspan or has_nbsp
return not is_not_empty
def _remove_empty_table_rows_comprehensive(self, html_content: str) -> str: def _remove_empty_table_rows_comprehensive(self, html_content: str) -> str:
"""
Комплексное удаление пустых строк таблиц ВНУТРИ таблиц
Убирает строки без текстового содержимого внутри <table>...</table>
"""
try: try:
# Находим все таблицы
tables = list( tables = list(
re.finditer( re.finditer(
r"<table[^>]*>.*?</table>", r"<table[^>]*>.*?</table>",
@@ -76,60 +53,35 @@ class WeasyPrintConverter:
if not tables: if not tables:
return html_content return html_content
# Обрабатываем с конца, чтобы не сбивать индексы
for table_match in reversed(tables): for table_match in reversed(tables):
table_html = table_match.group(0) table_html = table_match.group(0)
table_start = table_match.start() table_start = table_match.start()
table_end = table_match.end() table_end = table_match.end()
# Разбираем таблицу на строки
rows = [] rows = []
pos = 0 pos = 0
table_length = len(table_html) table_length = len(table_html)
while pos < table_length: while pos < table_length:
# Ищем начало строки
tr_start = table_html.lower().find("<tr", pos) tr_start = table_html.lower().find("<tr", pos)
if tr_start == -1: if tr_start == -1:
break break
# Ищем конец строки
tr_end = table_html.lower().find("</tr>", tr_start) tr_end = table_html.lower().find("</tr>", tr_start)
if tr_end == -1: if tr_end == -1:
# Нет закрывающего тега - ищем конец таблицы tr_end = table_length
tr_end = table_html.lower().find("</table>", tr_start) tr_end += 5
if tr_end == -1:
tr_end = table_length
tr_end += 5 # Длина '</tr>'
row_html = table_html[tr_start:tr_end] row_html = table_html[tr_start:tr_end]
# Проверяем строку на пустоту if not self._is_empty_table_row(row_html):
is_empty_row = self._is_empty_table_row(row_html)
if not is_empty_row:
# Сохраняем непустую строку
rows.append(row_html) rows.append(row_html)
else:
logger.debug(f"Удалена пустая строка таблицы")
pos = tr_end pos = tr_end
if len(rows) == 0: if len(rows) == 0:
# Если все строки пустые, удаляем всю таблицу
logger.info(f"🗑️ Удалена полностью пустая таблица")
html_content = html_content[:table_start] + html_content[table_end:] html_content = html_content[:table_start] + html_content[table_end:]
else: else:
# Собираем таблицу обратно
new_table_html = table_html
for row in rows:
# Убедимся, что строка уже в таблице
if row not in new_table_html:
# Это не должно происходить, но на всякий случай
pass
# Заменяем оригинальную таблицу (она осталась неизменной если строки не удалялись)
# Но мы все равно пересобираем для чистоты
table_header_end = ( table_header_end = (
table_html.lower().find(">", table_html.lower().find("<table")) table_html.lower().find(">", table_html.lower().find("<table"))
+ 1 + 1
@@ -141,6 +93,7 @@ class WeasyPrintConverter:
+ "".join(rows) + "".join(rows)
+ table_html[table_footer_start:] + table_html[table_footer_start:]
) )
html_content = ( html_content = (
html_content[:table_start] html_content[:table_start]
+ new_table + new_table
@@ -153,52 +106,24 @@ class WeasyPrintConverter:
logger.warning(f"⚠ Ошибка удаления пустых строк таблиц: {e}") logger.warning(f"⚠ Ошибка удаления пустых строк таблиц: {e}")
return html_content return html_content
def _is_empty_table_row(self, row_html: str) -> bool: # ---------------------------------------------------------
""" # ALIGNMENT FIX
Проверяет, является ли строка таблицы пустой # ---------------------------------------------------------
"""
# Убираем все HTML теги
text_only = re.sub(r"<[^>]+>", "", row_html)
# Убираем все пробелы, невидимые символы и спецсимволы
clean_text = re.sub(r"[\s\xa0\u200b\u200c\u200d\u2060\ufeff]+", "", text_only)
# Убираем HTML entities (но оставляем &nbsp; как признак непустоты)
clean_text = re.sub(r"&[^;]{2,6};", "", clean_text)
# Проверяем специальные случаи
has_colspan = "colspan" in row_html.lower()
has_rowspan = "rowspan" in row_html.lower()
has_nbsp = "&nbsp;" in row_html or "&#160;" in row_html
# Строка считается НЕ пустой если:
# 1. Есть текст после очистки
# 2. Есть объединенные ячейки
# 3. Есть неразрывные пробелы
is_not_empty = bool(clean_text) or has_colspan or has_rowspan or has_nbsp
return not is_not_empty
def _center_elements_with_center_alignment(self, html_content: str) -> str: def _center_elements_with_center_alignment(self, html_content: str) -> str:
"""
Находит элементы с text-align: center и гарантирует их центрирование
"""
try: try:
# Ищем элементы со стилем text-align: center
def add_center_alignment(match): def add_center_alignment(match):
full_match = match.group(0) full_match = match.group(0)
element_with_style = match.group(1) element_with_style = match.group(1)
element_name = match.group(2) element_name = match.group(2)
style_content = match.group(3)
# Если уже есть align="center", оставляем как есть
if ( if (
'align="center"' in element_with_style 'align="center"' in element_with_style
or "align='center'" in element_with_style or "align='center'" in element_with_style
): ):
return full_match return full_match
# Добавляем align="center" к элементу
if element_name in [ if element_name in [
"p", "p",
"div", "div",
@@ -211,289 +136,103 @@ class WeasyPrintConverter:
"td", "td",
"th", "th",
]: ]:
new_element = element_with_style.replace( return element_with_style.replace(
f"<{element_name}", f'<{element_name} align="center"' f"<{element_name}", f'<{element_name} align="center"'
) ) + match.group(4)
return new_element + match.group(4)
return full_match return full_match
# Паттерн для поиска элементов с text-align: center
pattern = r'(<(\w+)[^>]*style\s*=\s*["\'][^"\']*text-align\s*:\s*center[^"\']*["\'][^>]*>)(.*?)' pattern = r'(<(\w+)[^>]*style\s*=\s*["\'][^"\']*text-align\s*:\s*center[^"\']*["\'][^>]*>)(.*?)'
html_content = re.sub( return re.sub(
pattern, pattern,
add_center_alignment, add_center_alignment,
html_content, html_content,
flags=re.IGNORECASE | re.DOTALL, flags=re.IGNORECASE | re.DOTALL,
) )
except Exception:
return html_content return html_content
except Exception as e: # ---------------------------------------------------------
# logger.warning(f"⚠ Ошибка центрирования элементов: {e}") # HTML PREP
return html_content # ---------------------------------------------------------
def _prepare_html(self, html_content: str) -> str: def _prepare_html(self, html_content: str) -> str:
"""Подготавливает HTML для конвертации"""
try: try:
# 1. Удаляем изображения
html_content = re.sub(r"<img[^>]*>", "", html_content, flags=re.IGNORECASE) html_content = re.sub(r"<img[^>]*>", "", html_content, flags=re.IGNORECASE)
# 2. Убираем ВСЕ пустые строки таблиц (основная задача)
html_content = self._remove_empty_table_rows_comprehensive(html_content) html_content = self._remove_empty_table_rows_comprehensive(html_content)
# 3. Центрируем элементы с text-align: center
html_content = self._center_elements_with_center_alignment(html_content) html_content = self._center_elements_with_center_alignment(html_content)
# 4. Добавляем базовую структуру если её нет
if not html_content.strip().startswith("<!DOCTYPE"): if not html_content.strip().startswith("<!DOCTYPE"):
html_content = f"""<!DOCTYPE html> html_content = f"""<!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<style> <style>
/* МИНИМАЛЬНЫЕ СТИЛИ - почти не вмешиваемся в оригинальный формат */ @page {{ size: A4; margin: 0.5cm; }}
body {{
@page {{ font-family: "DejaVu Sans", Arial, sans-serif;
size: A4; font-size: 9pt;
margin: 0.5cm !important; line-height: 1.0;
}} }}
table {{
/* Основные настройки */ border-collapse: collapse;
body {{ margin: 0;
font-family: "DejaVu Sans", Arial, sans-serif; font-size: 8.5pt;
font-size: 9pt !important; }}
line-height: 1.0 !important; th, td {{
color: #000000; padding: 3px 5px;
}} vertical-align: middle;
}}
/* ТАБЛИЦЫ - сохраняем оригинальный формат! */ th {{
table {{ text-align: center;
border-collapse: collapse !important; }}
margin: 0 !important; /* Убрали все отступы вокруг таблиц */ </style>
font-size: 8.5pt !important;
page-break-inside: auto !important;
}}
/* ЯЧЕЙКИ ТАБЛИЦ - только вертикальное центрирование и небольшие отступы */
th, td {{
padding: 3px 5px !important; /* Минимальные отступы */
vertical-align: middle !important; /* Вертикальное центрирование */
}}
/* НЕ добавляем границы автоматически! */
/* Границы будут ТОЛЬКО там, где они были в оригинале */
/* Горизонтальное центрирование заголовков таблиц */
th {{
text-align: center !important;
}}
/* Элементы с align="center" */
[align="center"] {{
text-align: center !important;
}}
/* Элементы со style*="text-align: center" */
[style*="text-align: center"] {{
text-align: center !important;
}}
/* Заголовки документа */
h1, h2, h3, h4, h5, h6 {{
font-weight: bold !important;
margin-top: 8px !important;
margin-bottom: 4px !important;
}}
h1 {{ font-size: 11pt !important; page-break-before: always !important; }}
h2 {{ font-size: 10pt !important; }}
h3 {{ font-size: 9.5pt !important; }}
/* Параграфы - минимальные отступы */
p {{
margin: 0 0 3px 0 !important;
}}
/* Форматирование текста */
strong, b {{
font-weight: bold !important;
}}
em, i {{
font-style: italic !important;
}}
/* Списки */
ul, ol {{
margin: 3px 0 3px 15px !important;
padding: 0 !important;
}}
li {{
margin-bottom: 1px !important;
}}
/* Убираем лишние отступы */
div, p, span {{
margin: 0 !important;
padding: 0 !important;
}}
br {{
line-height: 0.5 !important;
margin: 0 !important;
padding: 0 !important;
}}
/* Убираем отступы перед таблицами на уровне CSS */
:before(table) {{
margin-bottom: 0 !important;
padding-bottom: 0 !important;
}}
</style>
</head> </head>
<body> <body>
{html_content} {html_content}
</body> </body>
</html>""" </html>"""
# 5. Убеждаемся в наличии charset
if "<meta charset=" not in html_content.lower():
html_content = html_content.replace(
"<head>", '<head><meta charset="UTF-8">', 1
)
# 6. ОБРАБОТКА ТАБЛИЦ:
# Только добавляем вертикальное центрирование и отступы, НЕ меняем границы!
# Добавляем vertical-align: middle ко всем ячейкам если его нет
def add_vertical_align(match):
cell_tag = match.group(0)
if (
"valign=" not in cell_tag.lower()
and "vertical-align:" not in cell_tag.lower()
):
return cell_tag.replace("<td", '<td valign="middle"').replace(
"<th", '<th valign="middle"'
)
return cell_tag
html_content = re.sub(
r"<(td|th)(?![^>]*(?:valign|vertical-align))[^>]*>",
add_vertical_align,
html_content,
flags=re.IGNORECASE,
)
# Добавляем text-align: center к заголовкам таблиц если нет своего выравнивания
def add_th_center(match):
th_tag = match.group(0)
if (
"align=" not in th_tag.lower()
and "text-align:" not in th_tag.lower()
):
return th_tag.replace("<th", '<th align="center"')
return th_tag
html_content = re.sub(
r"<th[^>]*>", add_th_center, html_content, flags=re.IGNORECASE
)
# 7. ФИНАЛЬНАЯ ПРОВЕРКА: еще раз удаляем пустые строки таблиц после всех модификаций
html_content = self._remove_empty_table_rows_comprehensive(html_content)
logger.debug(f"HTML подготовлен, размер: {len(html_content)} символов")
return html_content return html_content
except Exception as e: except Exception as e:
logger.error(f"❌ Ошибка подготовки HTML: {e}") logger.error(f"❌ Ошибка подготовки HTML: {e}")
# Минимальная обработка в случае ошибки return html_content
return f"""<!DOCTYPE html>
<html> # ---------------------------------------------------------
<head> # MAIN CONVERSION
<meta charset="UTF-8"> # ---------------------------------------------------------
<style>
@page {{ margin: 0.5cm; }}
body {{ font-size: 9pt; line-height: 1.0; }}
table {{ border-collapse: collapse; margin: 0; }}
th, td {{ padding: 3px 5px; vertical-align: middle; }}
th {{ text-align: center; }}
</style>
</head>
<body>{html_content}</body>
</html>"""
def convert_html_to_pdf( def convert_html_to_pdf(
self, html_content: str, output_path: Path self, html_content: str, output_path: Path
) -> Tuple[bool, str]: ) -> Tuple[bool, str]:
"""
Конвертирует HTML в PDF и сохраняет во все указанные папки
Оптимизированная версия: конвертирует один раз, копирует результат
"""
if not self.weasyprint_path:
return False, "WeasyPrint не найден. Установите: brew install weasyprint"
try: try:
prepared_html = self._prepare_html(html_content) prepared_html = self._prepare_html(html_content)
with tempfile.NamedTemporaryFile( HTML(string=prepared_html, base_url=str(self.output_dir)).write_pdf(
mode="w", suffix=".html", delete=False, encoding="utf-8" target=str(output_path), presentational_hints=True
) as f: )
f.write(prepared_html)
temp_html = f.name
try: if not output_path.exists() or output_path.stat().st_size < 1024:
cmd = [ return False, "Созданный PDF слишком мал или отсутствует"
self.weasyprint_path,
temp_html,
output_path,
"--encoding",
"utf-8",
"--presentational-hints",
]
result = subprocess.run( return True, f"✅ PDF создан: {output_path}"
cmd, capture_output=True, text=True, timeout=180
)
if result.returncode != 0:
error_msg = (
result.stderr[:300] if result.stderr else "Неизвестная ошибка"
)
return False, f"Ошибка конвертации: {error_msg}"
if not output_path.exists() or output_path.stat().st_size < 1024:
return False, "Созданный PDF слишком мал или отсутствует"
# Формируем результат
return (
True,
f"✅ PDF создан и сохранен: {output_path}",
)
finally:
if os.path.exists(temp_html):
os.unlink(temp_html)
except subprocess.TimeoutExpired:
return False, "Таймаут при конвертации (180 секунд)"
except Exception as e: except Exception as e:
logger.error(f"Неожиданная ошибка: {str(e)}") logger.error(f"Ошибка конвертации: {e}")
return False, f"Ошибка конвертации: {str(e)}" return False, str(e)
# ---------------------------------------------------------
# BATCH PROCESSING
# ---------------------------------------------------------
def convert_documents(self, docs: List[Dict]) -> Dict[str, Dict]: def convert_documents(self, docs: List[Dict]) -> Dict[str, Dict]:
results = { results = {"status": "SUCCESS", "files": []}
"status": "SUCCESS",
"files": [],
}
if len(docs) > self.max_files: if len(docs) > self.max_files:
logger.warning(
f"⚠ Получено {len(docs)} документов, обработано {self.max_files}"
)
docs = docs[: self.max_files] docs = docs[: self.max_files]
for i, doc in enumerate(docs, 1): for i, doc in enumerate(docs, 1):
@@ -513,34 +252,26 @@ class WeasyPrintConverter:
} }
) )
logger.info(f"📄 Документ {i}/{len(docs)}: {filename}")
if not html_content or len(html_content.strip()) < 100: if not html_content or len(html_content.strip()) < 100:
results["status"] = "ERROR" results["status"] = "ERROR"
logger.warning(f" ⚠ Пропущен: недостаточно контента")
continue continue
success, message = self.convert_html_to_pdf(html_content, output_path) success, message = self.convert_html_to_pdf(html_content, output_path)
if success: if not success:
logger.info(f" ✅ Успешно") results["status"] = "ERROR"
else: logger.error(message)
logger.error(f" ❌ Ошибка: {message}")
return results return results
def convert_docs_to_pdfs(docs: List[Dict], idPatientMis: str) -> Dict: def convert_docs_to_pdfs(docs: List[Dict], idPatientMis: str) -> Dict:
try: try:
baseDir = "src/attachments/export/" base_dir = "attachments/export/"
outputDir = f"{baseDir}{idPatientMis}" output_dir = f"{base_dir}{idPatientMis}"
converter = WeasyPrintConverter(outputDir) converter = WeasyPrintConverter(output_dir)
return converter.convert_documents(docs) return converter.convert_documents(docs)
except Exception as e: except Exception as e:
logger.error(f"❌ Критическая ошибка в convert_docs_to_pdfs: {str(e)}") logger.error(f"❌ Критическая ошибка: {e}")
return {"status": "ERROR", "message": str(e)}
return {
"status": "ERROR",
"message": f"Критическая ошибка в convert_docs_to_pdfs: {str(e)}",
}
-1
View File
@@ -22,7 +22,6 @@ async def render(
context = { context = {
"request": request, "request": request,
"content": {}, "content": {},
# "content": {"app_secret": config.APP_SECRET},
} }
fileName = f"{request.scope['path']}/index.html" fileName = f"{request.scope['path']}/index.html"
+15
View File
@@ -0,0 +1,15 @@
#!/bin/sh
set -e # Выход при ошибке
cd /app/src
echo "Running database initialization..."
uv run init_db.py
echo "Starting Gunicorn..."
exec /app/.venv/bin/gunicorn \
-w 1 \
-k uvicorn.workers.UvicornWorker \
"init:app" \
-b "0.0.0.0:80" \
--log-config="/app/src/config/logger.ini"
Generated
+269 -4
View File
@@ -230,6 +230,50 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" }, { url = "https://files.pythonhosted.org/packages/3a/2a/7cc015f5b9f5db42b7d48157e23356022889fc354a2813c15934b7cb5c0e/attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373", size = 67615, upload-time = "2025-10-06T13:54:43.17Z" },
] ]
[[package]]
name = "brotli"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f7/16/c92ca344d646e71a43b8bb353f0a6490d7f6e06210f8554c8f874e454285/brotli-1.2.0.tar.gz", hash = "sha256:e310f77e41941c13340a95976fe66a8a95b01e783d430eeaf7a2f87e0a57dd0a", size = 7388632, upload-time = "2025-11-05T18:39:42.86Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6c/d4/4ad5432ac98c73096159d9ce7ffeb82d151c2ac84adcc6168e476bb54674/brotli-1.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9e5825ba2c9998375530504578fd4d5d1059d09621a02065d1b6bfc41a8e05ab", size = 861523, upload-time = "2025-11-05T18:38:34.67Z" },
{ url = "https://files.pythonhosted.org/packages/91/9f/9cc5bd03ee68a85dc4bc89114f7067c056a3c14b3d95f171918c088bf88d/brotli-1.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0cf8c3b8ba93d496b2fae778039e2f5ecc7cff99df84df337ca31d8f2252896c", size = 444289, upload-time = "2025-11-05T18:38:35.6Z" },
{ url = "https://files.pythonhosted.org/packages/2e/b6/fe84227c56a865d16a6614e2c4722864b380cb14b13f3e6bef441e73a85a/brotli-1.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8565e3cdc1808b1a34714b553b262c5de5fbda202285782173ec137fd13709f", size = 1528076, upload-time = "2025-11-05T18:38:36.639Z" },
{ url = "https://files.pythonhosted.org/packages/55/de/de4ae0aaca06c790371cf6e7ee93a024f6b4bb0568727da8c3de112e726c/brotli-1.2.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:26e8d3ecb0ee458a9804f47f21b74845cc823fd1bb19f02272be70774f56e2a6", size = 1626880, upload-time = "2025-11-05T18:38:37.623Z" },
{ url = "https://files.pythonhosted.org/packages/5f/16/a1b22cbea436642e071adcaf8d4b350a2ad02f5e0ad0da879a1be16188a0/brotli-1.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67a91c5187e1eec76a61625c77a6c8c785650f5b576ca732bd33ef58b0dff49c", size = 1419737, upload-time = "2025-11-05T18:38:38.729Z" },
{ url = "https://files.pythonhosted.org/packages/46/63/c968a97cbb3bdbf7f974ef5a6ab467a2879b82afbc5ffb65b8acbb744f95/brotli-1.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ecdb3b6dc36e6d6e14d3a1bdc6c1057c8cbf80db04031d566eb6080ce283a48", size = 1484440, upload-time = "2025-11-05T18:38:39.916Z" },
{ url = "https://files.pythonhosted.org/packages/06/9d/102c67ea5c9fc171f423e8399e585dabea29b5bc79b05572891e70013cdd/brotli-1.2.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3e1b35d56856f3ed326b140d3c6d9db91740f22e14b06e840fe4bb1923439a18", size = 1593313, upload-time = "2025-11-05T18:38:41.24Z" },
{ url = "https://files.pythonhosted.org/packages/9e/4a/9526d14fa6b87bc827ba1755a8440e214ff90de03095cacd78a64abe2b7d/brotli-1.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:54a50a9dad16b32136b2241ddea9e4df159b41247b2ce6aac0b3276a66a8f1e5", size = 1487945, upload-time = "2025-11-05T18:38:42.277Z" },
{ url = "https://files.pythonhosted.org/packages/5b/e8/3fe1ffed70cbef83c5236166acaed7bb9c766509b157854c80e2f766b38c/brotli-1.2.0-cp313-cp313-win32.whl", hash = "sha256:1b1d6a4efedd53671c793be6dd760fcf2107da3a52331ad9ea429edf0902f27a", size = 334368, upload-time = "2025-11-05T18:38:43.345Z" },
{ url = "https://files.pythonhosted.org/packages/ff/91/e739587be970a113b37b821eae8097aac5a48e5f0eca438c22e4c7dd8648/brotli-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:b63daa43d82f0cdabf98dee215b375b4058cce72871fd07934f179885aad16e8", size = 369116, upload-time = "2025-11-05T18:38:44.609Z" },
{ url = "https://files.pythonhosted.org/packages/17/e1/298c2ddf786bb7347a1cd71d63a347a79e5712a7c0cba9e3c3458ebd976f/brotli-1.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6c12dad5cd04530323e723787ff762bac749a7b256a5bece32b2243dd5c27b21", size = 863080, upload-time = "2025-11-05T18:38:45.503Z" },
{ url = "https://files.pythonhosted.org/packages/84/0c/aac98e286ba66868b2b3b50338ffbd85a35c7122e9531a73a37a29763d38/brotli-1.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3219bd9e69868e57183316ee19c84e03e8f8b5a1d1f2667e1aa8c2f91cb061ac", size = 445453, upload-time = "2025-11-05T18:38:46.433Z" },
{ url = "https://files.pythonhosted.org/packages/ec/f1/0ca1f3f99ae300372635ab3fe2f7a79fa335fee3d874fa7f9e68575e0e62/brotli-1.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:963a08f3bebd8b75ac57661045402da15991468a621f014be54e50f53a58d19e", size = 1528168, upload-time = "2025-11-05T18:38:47.371Z" },
{ url = "https://files.pythonhosted.org/packages/d6/a6/2ebfc8f766d46df8d3e65b880a2e220732395e6d7dc312c1e1244b0f074a/brotli-1.2.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9322b9f8656782414b37e6af884146869d46ab85158201d82bab9abbcb971dc7", size = 1627098, upload-time = "2025-11-05T18:38:48.385Z" },
{ url = "https://files.pythonhosted.org/packages/f3/2f/0976d5b097ff8a22163b10617f76b2557f15f0f39d6a0fe1f02b1a53e92b/brotli-1.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cf9cba6f5b78a2071ec6fb1e7bd39acf35071d90a81231d67e92d637776a6a63", size = 1419861, upload-time = "2025-11-05T18:38:49.372Z" },
{ url = "https://files.pythonhosted.org/packages/9c/97/d76df7176a2ce7616ff94c1fb72d307c9a30d2189fe877f3dd99af00ea5a/brotli-1.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7547369c4392b47d30a3467fe8c3330b4f2e0f7730e45e3103d7d636678a808b", size = 1484594, upload-time = "2025-11-05T18:38:50.655Z" },
{ url = "https://files.pythonhosted.org/packages/d3/93/14cf0b1216f43df5609f5b272050b0abd219e0b54ea80b47cef9867b45e7/brotli-1.2.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1530af5c3c275b8524f2e24841cbe2599d74462455e9bae5109e9ff42e9361", size = 1593455, upload-time = "2025-11-05T18:38:51.624Z" },
{ url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164, upload-time = "2025-11-05T18:38:53.079Z" },
{ url = "https://files.pythonhosted.org/packages/64/6a/0c78d8f3a582859236482fd9fa86a65a60328a00983006bcf6d83b7b2253/brotli-1.2.0-cp314-cp314-win32.whl", hash = "sha256:832c115a020e463c2f67664560449a7bea26b0c1fdd690352addad6d0a08714d", size = 339280, upload-time = "2025-11-05T18:38:54.02Z" },
{ url = "https://files.pythonhosted.org/packages/f5/10/56978295c14794b2c12007b07f3e41ba26acda9257457d7085b0bb3bb90c/brotli-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:e7c0af964e0b4e3412a0ebf341ea26ec767fa0b4cf81abb5e897c9338b5ad6a3", size = 375639, upload-time = "2025-11-05T18:38:55.67Z" },
]
[[package]]
name = "brotlicffi"
version = "1.2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi" },
]
sdist = { url = "https://files.pythonhosted.org/packages/84/85/57c314a6b35336efbbdc13e5fc9ae13f6b60a0647cfa7c1221178ac6d8ae/brotlicffi-1.2.0.0.tar.gz", hash = "sha256:34345d8d1f9d534fcac2249e57a4c3c8801a33c9942ff9f8574f67a175e17adb", size = 476682, upload-time = "2025-11-21T18:17:57.334Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/df/a72b284d8c7bef0ed5756b41c2eb7d0219a1dd6ac6762f1c7bdbc31ef3af/brotlicffi-1.2.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:9458d08a7ccde8e3c0afedbf2c70a8263227a68dea5ab13590593f4c0a4fd5f4", size = 432340, upload-time = "2025-11-21T18:17:42.277Z" },
{ url = "https://files.pythonhosted.org/packages/74/2b/cc55a2d1d6fb4f5d458fba44a3d3f91fb4320aa14145799fd3a996af0686/brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84e3d0020cf1bd8b8131f4a07819edee9f283721566fe044a20ec792ca8fd8b7", size = 1534002, upload-time = "2025-11-21T18:17:43.746Z" },
{ url = "https://files.pythonhosted.org/packages/e4/9c/d51486bf366fc7d6735f0e46b5b96ca58dc005b250263525a1eea3cd5d21/brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33cfb408d0cff64cd50bef268c0fed397c46fbb53944aa37264148614a62e990", size = 1536547, upload-time = "2025-11-21T18:17:45.729Z" },
{ url = "https://files.pythonhosted.org/packages/1b/37/293a9a0a7caf17e6e657668bebb92dfe730305999fe8c0e2703b8888789c/brotlicffi-1.2.0.0-cp38-abi3-win32.whl", hash = "sha256:23e5c912fdc6fd37143203820230374d24babd078fc054e18070a647118158f6", size = 343085, upload-time = "2025-11-21T18:17:48.887Z" },
{ url = "https://files.pythonhosted.org/packages/07/6b/6e92009df3b8b7272f85a0992b306b61c34b7ea1c4776643746e61c380ac/brotlicffi-1.2.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:f139a7cdfe4ae7859513067b736eb44d19fae1186f9e99370092f6915216451b", size = 378586, upload-time = "2025-11-21T18:17:50.531Z" },
]
[[package]] [[package]]
name = "cffi" name = "cffi"
version = "2.0.0" version = "2.0.0"
@@ -308,6 +352,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/c1/e419ef3723a074172b68aaa89c9f3de486ed4c2399e2dbd8113a4fdcaf9e/colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c", size = 11743, upload-time = "2025-10-16T16:14:10.512Z" }, { url = "https://files.pythonhosted.org/packages/6d/c1/e419ef3723a074172b68aaa89c9f3de486ed4c2399e2dbd8113a4fdcaf9e/colorlog-6.10.1-py3-none-any.whl", hash = "sha256:2d7e8348291948af66122cff006c9f8da6255d224e7cf8e37d8de2df3bad8c9c", size = 11743, upload-time = "2025-10-16T16:14:10.512Z" },
] ]
[[package]]
name = "cssselect2"
version = "0.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tinycss2" },
{ name = "webencodings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e0/20/92eaa6b0aec7189fa4b75c890640e076e9e793095721db69c5c81142c2e1/cssselect2-0.9.0.tar.gz", hash = "sha256:759aa22c216326356f65e62e791d66160a0f9c91d1424e8d8adc5e74dddfc6fb", size = 35595, upload-time = "2026-02-12T17:16:39.614Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/21/0e/8459ca4413e1a21a06c97d134bfaf18adfd27cea068813dc0faae06cbf00/cssselect2-0.9.0-py3-none-any.whl", hash = "sha256:6a99e5f91f9a016a304dd929b0966ca464bcfda15177b6fb4a118fc0fb5d9563", size = 15453, upload-time = "2026-02-12T17:16:38.317Z" },
]
[[package]] [[package]]
name = "fastapi" name = "fastapi"
version = "0.128.0" version = "0.128.0"
@@ -323,6 +380,46 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" }, { url = "https://files.pythonhosted.org/packages/5c/05/5cbb59154b093548acd0f4c7c474a118eda06da25aa75c616b72d8fcd92a/fastapi-0.128.0-py3-none-any.whl", hash = "sha256:aebd93f9716ee3b4f4fcfe13ffb7cf308d99c9f3ab5622d8877441072561582d", size = 103094, upload-time = "2025-12-27T15:21:12.154Z" },
] ]
[[package]]
name = "fonttools"
version = "4.61.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ec/ca/cf17b88a8df95691275a3d77dc0a5ad9907f328ae53acbe6795da1b2f5ed/fonttools-4.61.1.tar.gz", hash = "sha256:6675329885c44657f826ef01d9e4fb33b9158e9d93c537d84ad8399539bc6f69", size = 3565756, upload-time = "2025-12-12T17:31:24.246Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4b/cf/00ba28b0990982530addb8dc3e9e6f2fa9cb5c20df2abdda7baa755e8fe1/fonttools-4.61.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c56c488ab471628ff3bfa80964372fc13504ece601e0d97a78ee74126b2045c", size = 2846454, upload-time = "2025-12-12T17:30:24.938Z" },
{ url = "https://files.pythonhosted.org/packages/5a/ca/468c9a8446a2103ae645d14fee3f610567b7042aba85031c1c65e3ef7471/fonttools-4.61.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dc492779501fa723b04d0ab1f5be046797fee17d27700476edc7ee9ae535a61e", size = 2398191, upload-time = "2025-12-12T17:30:27.343Z" },
{ url = "https://files.pythonhosted.org/packages/a3/4b/d67eedaed19def5967fade3297fed8161b25ba94699efc124b14fb68cdbc/fonttools-4.61.1-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:64102ca87e84261419c3747a0d20f396eb024bdbeb04c2bfb37e2891f5fadcb5", size = 4928410, upload-time = "2025-12-12T17:30:29.771Z" },
{ url = "https://files.pythonhosted.org/packages/b0/8d/6fb3494dfe61a46258cd93d979cf4725ded4eb46c2a4ca35e4490d84daea/fonttools-4.61.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c1b526c8d3f615a7b1867f38a9410849c8f4aef078535742198e942fba0e9bd", size = 4984460, upload-time = "2025-12-12T17:30:32.073Z" },
{ url = "https://files.pythonhosted.org/packages/f7/f1/a47f1d30b3dc00d75e7af762652d4cbc3dff5c2697a0dbd5203c81afd9c3/fonttools-4.61.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:41ed4b5ec103bd306bb68f81dc166e77409e5209443e5773cb4ed837bcc9b0d3", size = 4925800, upload-time = "2025-12-12T17:30:34.339Z" },
{ url = "https://files.pythonhosted.org/packages/a7/01/e6ae64a0981076e8a66906fab01539799546181e32a37a0257b77e4aa88b/fonttools-4.61.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b501c862d4901792adaec7c25b1ecc749e2662543f68bb194c42ba18d6eec98d", size = 5067859, upload-time = "2025-12-12T17:30:36.593Z" },
{ url = "https://files.pythonhosted.org/packages/73/aa/28e40b8d6809a9b5075350a86779163f074d2b617c15d22343fce81918db/fonttools-4.61.1-cp313-cp313-win32.whl", hash = "sha256:4d7092bb38c53bbc78e9255a59158b150bcdc115a1e3b3ce0b5f267dc35dd63c", size = 2267821, upload-time = "2025-12-12T17:30:38.478Z" },
{ url = "https://files.pythonhosted.org/packages/1a/59/453c06d1d83dc0951b69ef692d6b9f1846680342927df54e9a1ca91c6f90/fonttools-4.61.1-cp313-cp313-win_amd64.whl", hash = "sha256:21e7c8d76f62ab13c9472ccf74515ca5b9a761d1bde3265152a6dc58700d895b", size = 2318169, upload-time = "2025-12-12T17:30:40.951Z" },
{ url = "https://files.pythonhosted.org/packages/32/8f/4e7bf82c0cbb738d3c2206c920ca34ca74ef9dabde779030145d28665104/fonttools-4.61.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fff4f534200a04b4a36e7ae3cb74493afe807b517a09e99cb4faa89a34ed6ecd", size = 2846094, upload-time = "2025-12-12T17:30:43.511Z" },
{ url = "https://files.pythonhosted.org/packages/71/09/d44e45d0a4f3a651f23a1e9d42de43bc643cce2971b19e784cc67d823676/fonttools-4.61.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d9203500f7c63545b4ce3799319fe4d9feb1a1b89b28d3cb5abd11b9dd64147e", size = 2396589, upload-time = "2025-12-12T17:30:45.681Z" },
{ url = "https://files.pythonhosted.org/packages/89/18/58c64cafcf8eb677a99ef593121f719e6dcbdb7d1c594ae5a10d4997ca8a/fonttools-4.61.1-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa646ecec9528bef693415c79a86e733c70a4965dd938e9a226b0fc64c9d2e6c", size = 4877892, upload-time = "2025-12-12T17:30:47.709Z" },
{ url = "https://files.pythonhosted.org/packages/8a/ec/9e6b38c7ba1e09eb51db849d5450f4c05b7e78481f662c3b79dbde6f3d04/fonttools-4.61.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f35ad7805edba3aac1a3710d104592df59f4b957e30108ae0ba6c10b11dd75", size = 4972884, upload-time = "2025-12-12T17:30:49.656Z" },
{ url = "https://files.pythonhosted.org/packages/5e/87/b5339da8e0256734ba0dbbf5b6cdebb1dd79b01dc8c270989b7bcd465541/fonttools-4.61.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b931ae8f62db78861b0ff1ac017851764602288575d65b8e8ff1963fed419063", size = 4924405, upload-time = "2025-12-12T17:30:51.735Z" },
{ url = "https://files.pythonhosted.org/packages/0b/47/e3409f1e1e69c073a3a6fd8cb886eb18c0bae0ee13db2c8d5e7f8495e8b7/fonttools-4.61.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b148b56f5de675ee16d45e769e69f87623a4944f7443850bf9a9376e628a89d2", size = 5035553, upload-time = "2025-12-12T17:30:54.823Z" },
{ url = "https://files.pythonhosted.org/packages/bf/b6/1f6600161b1073a984294c6c031e1a56ebf95b6164249eecf30012bb2e38/fonttools-4.61.1-cp314-cp314-win32.whl", hash = "sha256:9b666a475a65f4e839d3d10473fad6d47e0a9db14a2f4a224029c5bfde58ad2c", size = 2271915, upload-time = "2025-12-12T17:30:57.913Z" },
{ url = "https://files.pythonhosted.org/packages/52/7b/91e7b01e37cc8eb0e1f770d08305b3655e4f002fc160fb82b3390eabacf5/fonttools-4.61.1-cp314-cp314-win_amd64.whl", hash = "sha256:4f5686e1fe5fce75d82d93c47a438a25bf0d1319d2843a926f741140b2b16e0c", size = 2323487, upload-time = "2025-12-12T17:30:59.804Z" },
{ url = "https://files.pythonhosted.org/packages/39/5c/908ad78e46c61c3e3ed70c3b58ff82ab48437faf84ec84f109592cabbd9f/fonttools-4.61.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e76ce097e3c57c4bcb67c5aa24a0ecdbd9f74ea9219997a707a4061fbe2707aa", size = 2929571, upload-time = "2025-12-12T17:31:02.574Z" },
{ url = "https://files.pythonhosted.org/packages/bd/41/975804132c6dea64cdbfbaa59f3518a21c137a10cccf962805b301ac6ab2/fonttools-4.61.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:9cfef3ab326780c04d6646f68d4b4742aae222e8b8ea1d627c74e38afcbc9d91", size = 2435317, upload-time = "2025-12-12T17:31:04.974Z" },
{ url = "https://files.pythonhosted.org/packages/b0/5a/aef2a0a8daf1ebaae4cfd83f84186d4a72ee08fd6a8451289fcd03ffa8a4/fonttools-4.61.1-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a75c301f96db737e1c5ed5fd7d77d9c34466de16095a266509e13da09751bd19", size = 4882124, upload-time = "2025-12-12T17:31:07.456Z" },
{ url = "https://files.pythonhosted.org/packages/80/33/d6db3485b645b81cea538c9d1c9219d5805f0877fda18777add4671c5240/fonttools-4.61.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:91669ccac46bbc1d09e9273546181919064e8df73488ea087dcac3e2968df9ba", size = 5100391, upload-time = "2025-12-12T17:31:09.732Z" },
{ url = "https://files.pythonhosted.org/packages/6c/d6/675ba631454043c75fcf76f0ca5463eac8eb0666ea1d7badae5fea001155/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c33ab3ca9d3ccd581d58e989d67554e42d8d4ded94ab3ade3508455fe70e65f7", size = 4978800, upload-time = "2025-12-12T17:31:11.681Z" },
{ url = "https://files.pythonhosted.org/packages/7f/33/d3ec753d547a8d2bdaedd390d4a814e8d5b45a093d558f025c6b990b554c/fonttools-4.61.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:664c5a68ec406f6b1547946683008576ef8b38275608e1cee6c061828171c118", size = 5006426, upload-time = "2025-12-12T17:31:13.764Z" },
{ url = "https://files.pythonhosted.org/packages/b4/40/cc11f378b561a67bea850ab50063366a0d1dd3f6d0a30ce0f874b0ad5664/fonttools-4.61.1-cp314-cp314t-win32.whl", hash = "sha256:aed04cabe26f30c1647ef0e8fbb207516fd40fe9472e9439695f5c6998e60ac5", size = 2335377, upload-time = "2025-12-12T17:31:16.49Z" },
{ url = "https://files.pythonhosted.org/packages/e4/ff/c9a2b66b39f8628531ea58b320d66d951267c98c6a38684daa8f50fb02f8/fonttools-4.61.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2180f14c141d2f0f3da43f3a81bc8aa4684860f6b0e6f9e165a4831f24e6a23b", size = 2400613, upload-time = "2025-12-12T17:31:18.769Z" },
{ url = "https://files.pythonhosted.org/packages/c7/4e/ce75a57ff3aebf6fc1f4e9d508b8e5810618a33d900ad6c19eb30b290b97/fonttools-4.61.1-py3-none-any.whl", hash = "sha256:17d2bf5d541add43822bcf0c43d7d847b160c9bb01d15d5007d84e2217aaa371", size = 1148996, upload-time = "2025-12-12T17:31:21.03Z" },
]
[package.optional-dependencies]
woff = [
{ name = "brotli", marker = "platform_python_implementation == 'CPython'" },
{ name = "brotlicffi", marker = "platform_python_implementation != 'CPython'" },
{ name = "zopfli" },
]
[[package]] [[package]]
name = "frozenlist" name = "frozenlist"
version = "1.8.0" version = "1.8.0"
@@ -430,6 +527,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/2b/98c7f93e6db9977aaee07eb1e51ca63bd5f779b900d362791d3252e60558/greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451", size = 233181, upload-time = "2026-01-23T15:33:00.29Z" }, { url = "https://files.pythonhosted.org/packages/e1/2b/98c7f93e6db9977aaee07eb1e51ca63bd5f779b900d362791d3252e60558/greenlet-3.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:301860987846c24cb8964bdec0e31a96ad4a2a801b41b4ef40963c1b44f33451", size = 233181, upload-time = "2026-01-23T15:33:00.29Z" },
] ]
[[package]]
name = "gunicorn"
version = "25.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/13/ef67f59f6a7896fdc2c1d62b5665c5219d6b0a9a1784938eb9a28e55e128/gunicorn-25.1.0.tar.gz", hash = "sha256:1426611d959fa77e7de89f8c0f32eed6aa03ee735f98c01efba3e281b1c47616", size = 594377, upload-time = "2026-02-13T11:09:58.989Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/da/73/4ad5b1f6a2e21cf1e85afdaad2b7b1a933985e2f5d679147a1953aaa192c/gunicorn-25.1.0-py3-none-any.whl", hash = "sha256:d0b1236ccf27f72cfe14bce7caadf467186f19e865094ca84221424e839b8b8b", size = 197067, upload-time = "2026-02-13T11:09:57.146Z" },
]
[[package]] [[package]]
name = "h11" name = "h11"
version = "0.16.0" version = "0.16.0"
@@ -587,6 +696,7 @@ dependencies = [
{ name = "colorlog" }, { name = "colorlog" },
{ name = "fastapi" }, { name = "fastapi" },
{ name = "greenlet" }, { name = "greenlet" },
{ name = "gunicorn" },
{ name = "jinja2" }, { name = "jinja2" },
{ name = "lxml" }, { name = "lxml" },
{ name = "pydantic" }, { name = "pydantic" },
@@ -595,6 +705,7 @@ dependencies = [
{ name = "python-multipart" }, { name = "python-multipart" },
{ name = "sqlalchemy" }, { name = "sqlalchemy" },
{ name = "uvicorn" }, { name = "uvicorn" },
{ name = "weasyprint" },
] ]
[package.metadata] [package.metadata]
@@ -607,6 +718,7 @@ requires-dist = [
{ name = "colorlog", specifier = ">=6.10.1" }, { name = "colorlog", specifier = ">=6.10.1" },
{ name = "fastapi", specifier = ">=0.128.0" }, { name = "fastapi", specifier = ">=0.128.0" },
{ name = "greenlet", specifier = ">=3.3.1" }, { name = "greenlet", specifier = ">=3.3.1" },
{ name = "gunicorn", specifier = ">=25.1.0" },
{ name = "jinja2", specifier = ">=3.1.6" }, { name = "jinja2", specifier = ">=3.1.6" },
{ name = "lxml", specifier = ">=6.0.2" }, { name = "lxml", specifier = ">=6.0.2" },
{ name = "pydantic", specifier = ">=2.12.5" }, { name = "pydantic", specifier = ">=2.12.5" },
@@ -614,7 +726,8 @@ requires-dist = [
{ name = "python-dotenv", specifier = ">=1.2.1" }, { name = "python-dotenv", specifier = ">=1.2.1" },
{ name = "python-multipart", specifier = ">=0.0.21" }, { name = "python-multipart", specifier = ">=0.0.21" },
{ name = "sqlalchemy", specifier = ">=2.0.46" }, { name = "sqlalchemy", specifier = ">=2.0.46" },
{ name = "uvicorn", specifier = ">=0.40.0" }, { name = "uvicorn", specifier = ">=0.41.0" },
{ name = "weasyprint", specifier = ">=68.1" },
] ]
[[package]] [[package]]
@@ -698,6 +811,73 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" }, { url = "https://files.pythonhosted.org/packages/b7/da/7d22601b625e241d4f23ef1ebff8acfc60da633c9e7e7922e24d10f592b3/multidict-6.7.0-py3-none-any.whl", hash = "sha256:394fc5c42a333c9ffc3e421a4c85e08580d990e08b99f6bf35b4132114c5dcb3", size = 12317, upload-time = "2025-10-06T14:52:29.272Z" },
] ]
[[package]]
name = "packaging"
version = "26.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
]
[[package]]
name = "pillow"
version = "12.1.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" },
{ url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" },
{ url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" },
{ url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" },
{ url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" },
{ url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" },
{ url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" },
{ url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" },
{ url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" },
{ url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" },
{ url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" },
{ url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" },
{ url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" },
{ url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" },
{ url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" },
{ url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" },
{ url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" },
{ url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" },
{ url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" },
{ url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" },
{ url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" },
{ url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" },
{ url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" },
{ url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" },
{ url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" },
{ url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" },
{ url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" },
{ url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" },
{ url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" },
{ url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" },
{ url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" },
{ url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" },
{ url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" },
{ url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" },
{ url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" },
{ url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" },
{ url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" },
{ url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" },
{ url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" },
{ url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" },
{ url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" },
{ url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" },
{ url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" },
{ url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" },
{ url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" },
{ url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" },
{ url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" },
{ url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" },
{ url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" },
{ url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" },
]
[[package]] [[package]]
name = "propcache" name = "propcache"
version = "0.4.1" version = "0.4.1"
@@ -844,6 +1024,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
] ]
[[package]]
name = "pydyf"
version = "0.12.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/36/ee/fb410c5c854b6a081a49077912a9765aeffd8e07cbb0663cfda310b01fb4/pydyf-0.12.1.tar.gz", hash = "sha256:fbd7e759541ac725c29c506612003de393249b94310ea78ae44cb1d04b220095", size = 17716, upload-time = "2025-12-02T14:52:14.244Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/11/47efe2f66ba848a107adfd490b508f5c0cedc82127950553dca44d29e6c4/pydyf-0.12.1-py3-none-any.whl", hash = "sha256:ea25b4e1fe7911195cb57067560daaa266639184e8335365cc3ee5214e7eaadc", size = 8028, upload-time = "2025-12-02T14:52:12.938Z" },
]
[[package]] [[package]]
name = "pyjwt" name = "pyjwt"
version = "2.10.1" version = "2.10.1"
@@ -853,6 +1042,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" },
] ]
[[package]]
name = "pyphen"
version = "0.17.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/69/56/e4d7e1bd70d997713649c5ce530b2d15a5fc2245a74ca820fc2d51d89d4d/pyphen-0.17.2.tar.gz", hash = "sha256:f60647a9c9b30ec6c59910097af82bc5dd2d36576b918e44148d8b07ef3b4aa3", size = 2079470, upload-time = "2025-01-20T13:18:36.296Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7b/1f/c2142d2edf833a90728e5cdeb10bdbdc094dde8dbac078cee0cf33f5e11b/pyphen-0.17.2-py3-none-any.whl", hash = "sha256:3a07fb017cb2341e1d9ff31b8634efb1ae4dc4b130468c7c39dd3d32e7c3affd", size = 2079358, upload-time = "2025-01-20T13:18:29.629Z" },
]
[[package]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.2.1" version = "1.2.1"
@@ -918,6 +1116,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" }, { url = "https://files.pythonhosted.org/packages/d9/52/1064f510b141bd54025f9b55105e26d1fa970b9be67ad766380a3c9b74b0/starlette-0.50.0-py3-none-any.whl", hash = "sha256:9e5391843ec9b6e472eed1365a78c8098cfceb7a74bfd4d6b1c0c0095efb3bca", size = 74033, upload-time = "2025-11-01T15:25:25.461Z" },
] ]
[[package]]
name = "tinycss2"
version = "1.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "webencodings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a3/ae/2ca4913e5c0f09781d75482874c3a95db9105462a92ddd303c7d285d3df2/tinycss2-1.5.1.tar.gz", hash = "sha256:d339d2b616ba90ccce58da8495a78f46e55d4d25f9fd71dfd526f07e7d53f957", size = 88195, upload-time = "2025-11-23T10:29:10.082Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/45/c7b5c3168458db837e8ceab06dc77824e18202679d0463f0e8f002143a97/tinycss2-1.5.1-py3-none-any.whl", hash = "sha256:3415ba0f5839c062696996998176c4a3751d18b7edaaeeb658c9ce21ec150661", size = 28404, upload-time = "2025-11-23T10:29:08.676Z" },
]
[[package]]
name = "tinyhtml5"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "webencodings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fd/03/6111ed99e9bf7dfa1c30baeef0e0fb7e0bd387bd07f8e5b270776fe1de3f/tinyhtml5-2.0.0.tar.gz", hash = "sha256:086f998833da24c300c414d9fe81d9b368fd04cb9d2596a008421cbc705fcfcc", size = 179507, upload-time = "2024-10-29T15:37:14.078Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/de/27c57899297163a4a84104d5cec0af3b1ac5faf62f44667e506373c6b8ce/tinyhtml5-2.0.0-py3-none-any.whl", hash = "sha256:13683277c5b176d070f82d099d977194b7a1e26815b016114f581a74bbfbf47e", size = 39793, upload-time = "2024-10-29T15:37:11.743Z" },
]
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.15.0" version = "4.15.0"
@@ -962,15 +1184,43 @@ wheels = [
[[package]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.40.0" version = "0.41.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "click" }, { name = "click" },
{ name = "h11" }, { name = "h11" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/c3/d1/8f3c683c9561a4e6689dd3b1d345c815f10f86acd044ee1fb9a4dcd0b8c5/uvicorn-0.40.0.tar.gz", hash = "sha256:839676675e87e73694518b5574fd0f24c9d97b46bea16df7b8c05ea1a51071ea", size = 81761, upload-time = "2025-12-21T14:16:22.45Z" } sdist = { url = "https://files.pythonhosted.org/packages/32/ce/eeb58ae4ac36fe09e3842eb02e0eb676bf2c53ae062b98f1b2531673efdd/uvicorn-0.41.0.tar.gz", hash = "sha256:09d11cf7008da33113824ee5a1c6422d89fbc2ff476540d69a34c87fab8b571a", size = 82633, upload-time = "2026-02-16T23:07:24.1Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/d8/2083a1daa7439a66f3a48589a57d576aa117726762618f6bb09fe3798796/uvicorn-0.40.0-py3-none-any.whl", hash = "sha256:c6c8f55bc8bf13eb6fa9ff87ad62308bbbc33d0b67f84293151efe87e0d5f2ee", size = 68502, upload-time = "2025-12-21T14:16:21.041Z" }, { url = "https://files.pythonhosted.org/packages/83/e4/d04a086285c20886c0daad0e026f250869201013d18f81d9ff5eada73a88/uvicorn-0.41.0-py3-none-any.whl", hash = "sha256:29e35b1d2c36a04b9e180d4007ede3bcb32a85fbdfd6c6aeb3f26839de088187", size = 68783, upload-time = "2026-02-16T23:07:22.357Z" },
]
[[package]]
name = "weasyprint"
version = "68.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi" },
{ name = "cssselect2" },
{ name = "fonttools", extra = ["woff"] },
{ name = "pillow" },
{ name = "pydyf" },
{ name = "pyphen" },
{ name = "tinycss2" },
{ name = "tinyhtml5" },
]
sdist = { url = "https://files.pythonhosted.org/packages/db/3e/65c0f176e6fb5c2b0a1ac13185b366f727d9723541babfa7fa4309998169/weasyprint-68.1.tar.gz", hash = "sha256:d3b752049b453a5c95edb27ce78d69e9319af5a34f257fa0f4c738c701b4184e", size = 1542379, upload-time = "2026-02-06T15:04:11.203Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/dd/dd/14eb73cea481ad8162d3b18a4850d4a84d6e804a22840cca207648532265/weasyprint-68.1-py3-none-any.whl", hash = "sha256:4dc3ba63c68bbbce3e9617cb2226251c372f5ee90a8a484503b1c099da9cf5be", size = 319789, upload-time = "2026-02-06T15:04:09.189Z" },
]
[[package]]
name = "webencodings"
version = "0.5.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" },
] ]
[[package]] [[package]]
@@ -1050,3 +1300,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" }, { url = "https://files.pythonhosted.org/packages/48/b7/503c98092fb3b344a179579f55814b613c1fbb1c23b3ec14a7b008a66a6e/yarl-1.22.0-cp314-cp314t-win_arm64.whl", hash = "sha256:9f6d73c1436b934e3f01df1e1b21ff765cd1d28c77dfb9ace207f746d4610ee1", size = 85171, upload-time = "2025-10-06T14:12:16.935Z" },
{ url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" }, { url = "https://files.pythonhosted.org/packages/73/ae/b48f95715333080afb75a4504487cbe142cae1268afc482d06692d605ae6/yarl-1.22.0-py3-none-any.whl", hash = "sha256:1380560bdba02b6b6c90de54133c81c9f2a453dee9912fe58c1dcced1edb7cff", size = 46814, upload-time = "2025-10-06T14:12:53.872Z" },
] ]
[[package]]
name = "zopfli"
version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0a/4d/a8cc1768b2eda3c0c7470bf8059dcb94ef96d45dd91fc6edd29430d44072/zopfli-0.4.1.tar.gz", hash = "sha256:07a5cdc5d1aaa6c288c5d9f5a5383042ba743641abf8e2fd898dcad622d8a38e", size = 179001, upload-time = "2026-02-13T14:17:27.156Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/2f/1a7082e9163ae3703b27d571720bf3c954a02a9cf1fdce47c51e70639256/zopfli-0.4.1-cp310-abi3-macosx_10_9_universal2.whl", hash = "sha256:4238d4d746d1095e29c9125490985e0c12ffd3654f54a24af551e2391e936d54", size = 291570, upload-time = "2026-02-13T14:17:12.556Z" },
{ url = "https://files.pythonhosted.org/packages/dd/6f/4a1a88edf9fa0ce102703f38ab4dfb285b7cd2dde5389184264ec759e06e/zopfli-0.4.1-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fdfb7ce9f5de37a5b2f75dd2642fd7717956ef2a72e0387302a36d382440db07", size = 829437, upload-time = "2026-02-13T14:17:14.431Z" },
{ url = "https://files.pythonhosted.org/packages/e3/77/d231012ddcaac9d2e184bd7808e106a8a0048855912e2e1c902b3f383413/zopfli-0.4.1-cp310-abi3-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7bcee1b189d64ec33d1e05cfa1b6a1268c29329c382f6ca1bd6245b04925c57", size = 818542, upload-time = "2026-02-13T14:17:16.353Z" },
{ url = "https://files.pythonhosted.org/packages/0d/4e/9b23690c4ca14fbeae2a8f7f6b2006611bf4cd7d5bcb2d9e6c718bd4b0e9/zopfli-0.4.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:27823dc1161a4031d1c25925fd45d9868ec0cbc7692341830a7dcfa25063662c", size = 1778034, upload-time = "2026-02-13T14:17:17.509Z" },
{ url = "https://files.pythonhosted.org/packages/e3/1b/51f7c28d4cde639cac4f5d47ff615548c1d9809f43cbacdd66eba5cd679d/zopfli-0.4.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5a4c22b6161f47f5bd34637dbaee6735abd287cd64e0d1ce28ef1871bf625f4b", size = 1863957, upload-time = "2026-02-13T14:17:19.259Z" },
{ url = "https://files.pythonhosted.org/packages/ae/4d/1ef17017d38eabe7ae28f18ef0f16d48966cc23a5657e4555fff61704539/zopfli-0.4.1-cp310-abi3-win32.whl", hash = "sha256:a899eca405662a23ae75054affa3517a060362eae1185d3d791c86a50153c4dd", size = 82314, upload-time = "2026-02-13T14:17:20.795Z" },
{ url = "https://files.pythonhosted.org/packages/0f/94/806bc84b389c7d70051d7c9a0179cff52de8b9f8dc2fc25bcf0bca302986/zopfli-0.4.1-cp310-abi3-win_amd64.whl", hash = "sha256:84a31ba9edc921b1d3a4449929394a993888f32d70de3a3617800c428a947b9b", size = 102186, upload-time = "2026-02-13T14:17:21.622Z" },
]