Merge remote-tracking branch 'lefeng/main' into merge-lefeng

pull/21891/head
ytqh 1 year ago
commit 7364cb70cf

@ -15,7 +15,7 @@ FROM base AS packages
# RUN sed -i 's@deb.debian.org@mirrors.aliyun.com@g' /etc/apt/sources.list.d/debian.sources
RUN apt-get update \
&& apt-get install -y --no-install-recommends gcc g++ libc-dev libffi-dev libgmp-dev libmpfr-dev libmpc-dev
&& apt-get install -y --no-install-recommends gcc g++ libc-dev libffi-dev libgmp-dev libmpfr-dev libmpc-dev libfreetype6-dev
# Install Python dependencies
COPY pyproject.toml uv.lock ./
@ -40,7 +40,27 @@ ENV TZ=UTC
WORKDIR /app/api
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl nodejs libgmp-dev libmpfr-dev libmpc-dev \
&& apt-get install -y --no-install-recommends \
curl \
nodejs \
libgmp-dev \
libmpfr-dev \
libmpc-dev \
libglib2.0-0 \
libpango-1.0-0 \
libpangoft2-1.0-0 \
libpangocairo-1.0-0 \
libharfbuzz0b \
libharfbuzz-gobject0 \
libharfbuzz-icu0 \
libharfbuzz-subset0 \
libcairo2 \
libcairo-gobject2 \
libcairo-script-interpreter2 \
libcairo2-dev \
libpango1.0-dev \
libpangocairo-1.0-0 \
libpangoxft-1.0-0 \
# if you located in China, you can use aliyun mirror to speed up
# && echo "deb http://mirrors.aliyun.com/debian testing main" > /etc/apt/sources.list \
# Don't replace the sources.list file, create a separate testing.list file

@ -26,22 +26,9 @@ from libs.password import hash_password, password_pattern, valid_password
from libs.rsa import generate_key_pair
from models import Account, Tenant, TenantAccountJoin
from models.account import TenantAccountJoinRole
from models.dataset import (
Dataset,
DatasetCollectionBinding,
DatasetMetadata,
DatasetMetadataBinding,
DocumentSegment,
)
from models.dataset import Dataset, DatasetCollectionBinding, DatasetMetadata, DatasetMetadataBinding, DocumentSegment
from models.dataset import Document as DatasetDocument
from models.model import (
App,
AppAnnotationSetting,
AppMode,
Conversation,
MessageAnnotation,
UploadFile,
)
from models.model import App, AppAnnotationSetting, AppMode, Conversation, MessageAnnotation, UploadFile
from models.provider import Provider, ProviderModel
from services.account_service import RegisterService, TenantService
from services.clear_free_plan_tenant_expired_logs import ClearFreePlanTenantExpiredLogs
@ -918,11 +905,7 @@ def create_admin_account(
"""
try:
# Check if organization exists
from models.organization import (
Organization,
OrganizationMember,
OrganizationRole,
)
from models.organization import Organization, OrganizationMember, OrganizationRole
organization = (
db.session.query(Organization)
@ -1180,11 +1163,7 @@ def update_organization_cmd(org_id, name, description, email_domains, status):
def list_organizations_cmd(tenant_id):
"""List all organizations with optional tenant filtering"""
try:
from models.organization import (
Organization,
OrganizationMember,
OrganizationRole,
)
from models.organization import Organization, OrganizationMember, OrganizationRole
query = db.session.query(Organization)

@ -0,0 +1,8 @@
from flask import Blueprint
from libs.external_api import ExternalApi
bp = Blueprint("inner_tools", __name__, url_prefix="/inner_tools")
api = ExternalApi(bp)
from . import answers_summary_analysis, html_to_pdf, markdown_to_pdf

@ -0,0 +1,555 @@
import json
import uuid
from typing import Any, Optional
import chardet
from controllers.inner_tools import api
from core.tools.tool_file_manager import ToolFileManager
from extensions.ext_database import db
from extensions.ext_storage import storage
from flask import jsonify, request
from flask_restful import Resource # type: ignore
from jinja2 import Template
from models.account import Tenant
from models.model import UploadFile
from models.workflow import WorkflowRun
from weasyprint import HTML
class AnswersSummaryAnalysisApi(Resource):
def post(self):
"""Analyze answers and provide summary statistics by category.
This endpoint takes a file_id of an answer sheet and a JSON payload of problem categories.
It reads the file, parses answers, and calculates success rates by category.
"""
# Parse request arguments
if not request.is_json:
return {"error": "Request must be JSON"}, 400
data = request.get_json()
workflow_run_id = data.get("workflow_run_id")
# read the arg of this workflow run
workflow_run = WorkflowRun.query.filter_by(id=workflow_run_id).first()
if not workflow_run:
return {"error": "workflow_run not found"}, 400
workflow_run_args = workflow_run.inputs
if not workflow_run_args:
return {"error": "workflow_run_args not found"}, 400
# get the file_id from the workflow_run_args
try:
args_json = json.loads(workflow_run_args)
user_answers_file_id = args_json.get("user_answers").get("related_id")
exam_answers_file_id = args_json.get("exam_answers").get("related_id")
except json.JSONDecodeError:
return {"error": "workflow_run_args must be a valid JSON string"}, 400
if not user_answers_file_id:
return {"error": "user_answers file_id is required"}, 400
if not exam_answers_file_id:
return {"error": "exam_answers file_id is required"}, 400
# Read the exam answers file to get categories and correct answers
exam_answers_file_content, _ = self._read_file_with_encoding_detection(
exam_answers_file_id
)
if not exam_answers_file_content:
return {"error": "Failed to read exam answers file or file not found"}, 404
# Parse the exam answers file
exam_answers, categories, correct_answer = self._parse_exam_answers(
exam_answers_file_content
)
if not categories or not correct_answer:
return {
"error": "Failed to parse categories and correct answers from exam file"
}, 400
# Read the user answers file content with encoding detection
user_answers_file_content, _ = self._read_file_with_encoding_detection(
user_answers_file_id
)
if not user_answers_file_content:
return {"error": "Failed to read user answers file or file not found"}, 404
# Parse the user answers
user_answers = self._parse_answers(user_answers_file_content)
if not user_answers:
return {"error": "Failed to parse user answers from file"}, 400
# Calculate category statistics
summary_analysis = self._calculate_category_statistics(
user_answers, correct_answer, categories
)
# Return the response
return jsonify(
{
"user_answers": user_answers,
"summary_analysis": summary_analysis,
"exam_answers": exam_answers,
}
)
def _read_file_with_encoding_detection(
self, file_id: str
) -> tuple[Optional[str], Optional[str]]:
"""Read file content with automatic encoding detection.
Supports both CSV and XLSX files, converting XLSX to CSV text format.
"""
try:
upload_file = (
db.session.query(UploadFile).filter(UploadFile.id == file_id).first()
)
if not upload_file:
return None, None
# Get the file content from storage
file_content = storage.load_once(upload_file.key)
# Check if the file is Excel (.xlsx) based on filename or mime type
file_extension = (
upload_file.name.split(".")[-1].lower() if upload_file.name else ""
)
mime_type = upload_file.mime_type if upload_file.mime_type else ""
is_excel = (
file_extension == "xlsx"
or mime_type
== "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
)
if is_excel:
# Process Excel file
import io
import pandas as pd
# Load Excel data
excel_data = io.BytesIO(file_content)
try:
# Read all sheets, default to first sheet
df = pd.read_excel(excel_data, engine="openpyxl")
# Convert DataFrame to CSV string
csv_content = df.to_csv(index=False)
return csv_content, "utf-8"
except Exception as e:
print(f"Error converting Excel file: {str(e)}")
return None, None
else:
# Process CSV file with encoding detection
# Detect the encoding
detection = chardet.detect(file_content)
encoding = detection.get("encoding", "utf-8")
# Try multiple encodings if needed
encodings_to_try = [
encoding,
"utf-8",
"gbk",
"gb2312",
"iso-8859-1",
"latin-1",
]
# Filter out any None values
encodings_to_try = [enc for enc in encodings_to_try if enc is not None]
decoded_content = None
detected_encoding = None
for enc in encodings_to_try:
try:
decoded_content = file_content.decode(enc)
detected_encoding = enc
break
except UnicodeDecodeError:
continue
return decoded_content, detected_encoding
except Exception as e:
print(f"Error reading file: {str(e)}")
return None, None
def _parse_exam_answers(
self, file_content: str
) -> tuple[list[dict[str, Any]], list[dict[str, Any]], list[str]]:
"""Parse exam answers from the file content.
Expected format is CSV with columns:
- 题号 (Question Number)
- 题目分类 (Category)
- 正确答案 (Correct Answer)
- 题目分析 (Question Analysis)
Returns:
Tuple containing:
- exam_answers: List of dictionaries with question details
- categories: List of categories with question numbers
- correct_answer: List of correct answers
"""
try:
import csv
from collections import defaultdict
from io import StringIO
# Create a CSV reader from the string content
csv_file = StringIO(file_content)
csv_reader = csv.reader(csv_file)
# Get the header row
header = next(csv_reader, None)
if not header:
return [], [], []
exam_answers = []
category_map = defaultdict(list)
correct_answers = [
""
] * 1000 # Initialize with empty strings, we'll trim later
max_question_num = 0
for row in csv_reader:
if not row or len(row) < 3: # Skip rows with insufficient data
continue
try:
question_num = int(row[0].strip())
category = row[1].strip()
correct_ans = row[2].strip()
analysis = row[3].strip() if len(row) > 3 else ""
# Record the maximum question number
max_question_num = max(max_question_num, question_num)
# Add to exam_answers
exam_answers.append(
{
"question_num": question_num,
"category": category,
"correct_answer": correct_ans,
"analysis": analysis,
}
)
# Map category to question numbers
category_map[category].append(str(question_num))
# Set correct answer
correct_answers[question_num - 1] = correct_ans
except (ValueError, IndexError):
continue
# Trim correct_answers to the maximum question number
correct_answers = correct_answers[:max_question_num]
# Convert category_map to the expected categories format
categories = [
{"name": cat, "items": items} for cat, items in category_map.items()
]
return exam_answers, categories, correct_answers
except Exception as e:
# Log the exception for debugging
print(f"Error parsing exam answers: {str(e)}")
return [], [], []
def _parse_answers(self, file_content: str) -> list[dict[str, Any]]:
"""Parse answers from the file content.
Expected format is CSV with the following structure:
- First column: Student ID (准考证号)
- Second column: Name (姓名)
- Third column: Score (得分)
- Remaining columns: Answers to questions (1, 2, 3, etc.)
"""
try:
import csv
from io import StringIO
# Create a CSV reader from the string content
csv_file = StringIO(file_content)
csv_reader = csv.reader(csv_file)
# Get the header row
header = next(csv_reader, None)
if not header:
return []
result = []
for row in csv_reader:
if (
not row or len(row) < 4
): # Skip empty rows or rows with insufficient data
continue
# Extract student ID and name
student_id = row[0].strip()
name = row[1].strip()
# Extract answers (skip ID, name, and score columns)
answers = [ans.strip() for ans in row[3:]]
result.append(
{"user_name": name, "code": student_id, "answers": answers}
)
return result
except Exception as e:
# Log the exception for debugging
print(f"Error parsing answers: {str(e)}")
return []
def _calculate_category_statistics(
self,
parsed_answers: list[dict[str, Any]],
correct_answer: list[str],
categories: list[dict[str, Any]],
) -> list[dict[str, Any]]:
"""Calculate statistics by category.
Args:
parsed_answers: List of dictionaries containing parsed student answers
correct_answer: List of correct answers for all questions
categories: List of question categories
Returns:
A list of dictionaries with category statistics:
[
{
"category": str,
"correct_rate": float,
"error_count": int,
"total_count": int
}
]
"""
summary = []
# For each category in the list
for category in categories:
# Extract category name and question numbers
if isinstance(category, dict):
# Original format with 'name' and 'items'
category_name = category.get("name", "")
question_numbers = category.get("items", [])
if not category_name or not question_numbers:
category_name = next(iter(category))
question_numbers = category.get(category_name, [])
elif isinstance(category, list) and len(category) == 2:
# New format: ['category_name', ['30', '36', '39', '50']]
category_name = category[0]
question_numbers = category[1]
else:
continue # Skip invalid category format
total_answers = 0
correct_answers = 0
for answer_data in parsed_answers:
answers = answer_data.get("answers", [])
# Check each question in this category
for q_num in question_numbers:
try:
# Convert to 0-based index
idx = int(q_num) - 1
if idx < 0 or idx >= len(answers) or idx >= len(correct_answer):
continue
student_answer = answers[idx].strip()
# Skip empty answers or placeholders
if not student_answer or student_answer in ["#", "?", "-"]:
continue
total_answers += 1
# Compare with correct answer (case insensitive)
if student_answer.lower() == correct_answer[idx].lower():
correct_answers += 1
except (ValueError, IndexError):
continue
# Calculate statistics
correct_rate = correct_answers / total_answers if total_answers > 0 else 0
error_count = total_answers - correct_answers
summary.append(
{
"category": category_name,
"correct_rate": round(correct_rate, 2),
"error_count": error_count,
"total_count": total_answers,
}
)
return summary
# ruff: noqa: E501
class GenerateAnalysisReportApi(Resource):
def post(self):
"""Generate a PDF analysis report based on the provided data."""
if not request.is_json:
return {"error": "Request must be JSON"}, 400
data = request.get_json()
summary_analysis = data.get("summary_analysis")
school_name = data.get(
"school_name", "山东单县一中"
) # Default value if not provided
html_template = data.get("html_template")
if not summary_analysis:
return {"error": "summary_analysis is required"}, 400
if not html_template: # default template
html_template = """
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
font-family: "SimHei", "Microsoft YaHei", sans-serif;
padding: 20px;
}
.title {
text-align: center;
font-size: 24px;
margin-bottom: 20px;
}
.subtitle {
text-align: center;
font-size: 18px;
color: #666;
margin-bottom: 30px;
}
.summary-section {
display: flex;
justify-content: space-between;
margin-bottom: 30px;
}
.summary-item {
margin: 10px 0;
}
.analysis-section {
margin-top: 20px;
}
.category-bar {
display: flex;
align-items: center;
margin: 10px 0;
}
.bar {
background-color: #e6f3ff;
height: 20px;
margin-right: 10px;
}
.stats {
color: #666;
}
.category-item {
margin-bottom: 20px;
}
</style>
</head>
<body>
<h1 class="title">模拟考分析报告</h1>
<div class="subtitle">Analysis of Examination</div>
<h2 class="title">{{ school_name }}</h2>
<div class="summary-section">
<div class="left-summary">
<div class="summary-item">总参考人数:</div>
<div class="summary-item">总平均分:</div>
<div class="summary-item">省内排名:</div>
</div>
<div class="right-summary">
<div class="summary-item">省内总人数:</div>
<div class="summary-item">省内平均分:</div>
<div class="summary-item">全国排名:</div>
</div>
</div>
<div class="analysis-section">
<h3>题目分析:</h3>
{% for category in summary_analysis %}
<div class="category-item">
<div>{{ category.category }}</div>
<div class="category-bar">
<div class="bar" style="width: 500px; position: relative; background-color: #e1e9f3;">
<div style="position: absolute; left: 0; top: 0; height: 100%; width: {% if category.error_count > 0 %}{% if (1 - category.correct_rate) * 100 < 0.1 %}0.1{% else %}{{ (1 - category.correct_rate) * 100 }}{% endif %}{% else %}0{% endif %}%; background-color: #7eb0e3; display: flex; align-items: center; justify-content: center;">
{% if category.error_count > 0 %}
<span style="color: #333; font-size: 14px;">做错{{ category.error_count }}</span>
{% endif %}
</div>
<div style="position: absolute; right: 10px; top: 0; height: 100%; display: flex; align-items: center;">
<span style="color: #666;">总解答数{{ category.total_count }}</span>
</div>
<div style="position: absolute; right: -130px; top: 0; height: 100%; display: flex; align-items: center;">
<span style="color: #666;">失分比{% if category.error_count == 0 %}0{% else %}{{ ((1 - category.correct_rate) * 100)|round }}{% endif %}%</span>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
</body>
</html>
"""
# Create the HTML with the template
template = Template(html_template)
html_content = template.render(
school_name=school_name, summary_analysis=summary_analysis
)
# Generate PDF
html = HTML(string=html_content)
pdf_file = html.write_pdf()
if pdf_file is None:
return {"error": "Failed to generate PDF"}, 500
# Get the first tenant from database (similar to markdown_to_pdf.py)
tenant = Tenant.query.first()
if not tenant:
return {"error": "No tenant found"}, 400
tenant_id = tenant.id
# Generate filename
filename = f"analysis_report_{school_name}_{uuid.uuid4().hex[:8]}.pdf"
# Save the file using ToolFileManager
tool_file = ToolFileManager().create_file_by_raw(
user_id=None,
tenant_id=tenant_id,
conversation_id=None,
file_binary=pdf_file,
mimetype="application/pdf",
)
# Return the file info with URL
file_url = ToolFileManager.sign_file(tool_file.id, ".pdf")
return jsonify(
{
"url": file_url,
"file_id": tool_file.id,
"file_name": filename,
"file_size": tool_file.size,
}
)
# ruff: noqa: E501
# Add API endpoints
api.add_resource(AnswersSummaryAnalysisApi, "/answers-summary-analysis")
api.add_resource(GenerateAnalysisReportApi, "/generate-analysis-report")

@ -0,0 +1,58 @@
import uuid
from flask import jsonify, request
from flask_restful import Resource # type: ignore
from weasyprint import HTML
from controllers.inner_tools import api
from core.tools.tool_file_manager import ToolFileManager
from models.account import Tenant
class HtmlToPdfApi(Resource):
def post(self):
"""Generate a PDF from the provided HTML content."""
if not request.data:
return {"error": "No HTML content provided"}, 400
html_content = request.data
# Generate PDF
html = HTML(string=html_content)
pdf_file = html.write_pdf()
if pdf_file is None:
return {"error": "Failed to generate PDF"}, 500
# Get the first tenant from database (similar to markdown_to_pdf.py)
tenant = Tenant.query.first()
if not tenant:
return {"error": "No tenant found"}, 400
tenant_id = tenant.id
# Generate filename
filename = f"html_to_pdf_{uuid.uuid4().hex[:8]}.pdf"
# Save the file using ToolFileManager
tool_file = ToolFileManager().create_file_by_raw(
user_id=None,
tenant_id=tenant_id,
conversation_id=None,
file_binary=pdf_file,
mimetype="application/pdf",
)
# Return the file info with URL
file_url = ToolFileManager.sign_file(tool_file.id, ".pdf")
return jsonify(
{
"url": file_url,
"file_id": tool_file.id,
"file_name": filename,
"file_size": tool_file.size,
}
)
api.add_resource(HtmlToPdfApi, "/html-to-pdf")

@ -0,0 +1,187 @@
import io
import uuid
from flask import Response, jsonify, request
from flask_restful import Resource # type: ignore
from reportlab.lib.pagesizes import letter
from reportlab.lib.styles import ParagraphStyle
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.cidfonts import UnicodeCIDFont
from reportlab.platypus import Paragraph, SimpleDocTemplate, Spacer
from controllers.inner_tools import api
from core.tools.tool_file_manager import ToolFileManager
from models.account import Tenant
class MarkdownToPDFApi(Resource):
def post(self):
"""Convert markdown to PDF
This endpoint takes markdown text and converts it to a PDF document.
Returns URL of uploaded PDF file.
No authentication required.
"""
# Parse request arguments
if not request.is_json:
return {"error": "Request must be JSON"}, 400
data = request.get_json()
markdown_text = data.get("markdown_text")
title = data.get("title", "Document")
if not markdown_text:
return {"error": "markdown_text is required"}, 400
# get first tenant from database
tenant = Tenant.query.first()
if not tenant:
return {"error": "no tenant found"}, 400
tenant_id = tenant.id
# Generate PDF
pdf_binary, filename = self._generate_pdf_binary(markdown_text, title)
# Save the file using ToolFileManager
tool_file = ToolFileManager().create_file_by_raw(
user_id=None,
tenant_id=tenant_id,
conversation_id=None,
file_binary=pdf_binary,
mimetype="application/pdf",
)
# Return the file info with URL
file_url = ToolFileManager.sign_file(tool_file.id, ".pdf")
return jsonify(
{
"url": file_url,
"file_id": tool_file.id,
"file_name": filename,
"file_size": tool_file.size,
}
)
def _generate_pdf_binary(self, markdown_text: str, title: str) -> tuple[bytes, str]:
"""Generate PDF from markdown text and return the binary data and filename"""
buffer = io.BytesIO()
# Create PDF document with proper margins
doc = SimpleDocTemplate(
buffer,
pagesize=letter,
rightMargin=72,
leftMargin=72,
topMargin=72,
bottomMargin=72,
)
# Register Chinese font
pdfmetrics.registerFont(UnicodeCIDFont("STSong-Light"))
# Define styles
title_style = ParagraphStyle(
"CustomTitle",
fontName="STSong-Light",
fontSize=16,
spaceAfter=30,
leading=20,
)
h1_style = ParagraphStyle(
"CustomH1",
fontName="STSong-Light",
fontSize=14,
spaceAfter=12,
spaceBefore=12,
leading=18,
)
h2_style = ParagraphStyle(
"CustomH2",
fontName="STSong-Light",
fontSize=13,
spaceAfter=10,
spaceBefore=10,
leading=16,
)
normal_style = ParagraphStyle(
"CustomNormal",
fontName="STSong-Light",
fontSize=12,
spaceAfter=8,
leading=14,
)
# Prepare content elements
elements = []
# Add title
elements.append(Paragraph(title, title_style))
elements.append(Spacer(1, 20))
# Process markdown content
lines = markdown_text.split("\n")
for line in lines:
if not line.strip():
elements.append(Spacer(1, 10))
continue
if line.startswith("# "):
elements.append(Paragraph(line[2:], h1_style))
elif line.startswith("## "):
elements.append(Paragraph(line[3:], h2_style))
elif line.startswith("### "):
elements.append(Paragraph(line[4:], h2_style))
else:
elements.append(Paragraph(line, normal_style))
# Build PDF
doc.build(elements)
buffer.seek(0)
# Generate filename
filename = f"{title.replace(' ', '_')}_{uuid.uuid4().hex[:8]}.pdf"
return buffer.getvalue(), filename
# Add API endpoint for getting PDF from stored file
class MarkdownToPDFFileApi(Resource):
def get(self, file_id):
"""Get a PDF file by its tool file ID
This endpoint retrieves a PDF file stored in the system using its tool file ID.
No authentication required.
"""
try:
# Get the file binary from ToolFileManager
result = ToolFileManager().get_file_binary(file_id)
if result is None:
return {"error": "File not found"}, 404
# Safely unpack result only after confirming it's not None
file_binary, mimetype = result
if mimetype != "application/pdf":
return {"error": "File is not a PDF"}, 400
# Return the PDF
response = Response(
file_binary,
mimetype="application/pdf",
headers={
"Content-Disposition": f'attachment; filename="{file_id}.pdf"',
"Content-Type": "application/pdf",
},
)
return response
except Exception as e:
return {"error": str(e)}, 500
api.add_resource(MarkdownToPDFApi, "/markdown-to-pdf")
api.add_resource(MarkdownToPDFFileApi, "/markdown-to-pdf/<string:file_id>")

@ -69,7 +69,7 @@ class ToolFileManager:
def create_file_by_raw(
self,
*,
user_id: str,
user_id: Optional[str],
tenant_id: str,
conversation_id: Optional[str],
file_binary: bytes,

@ -0,0 +1,37 @@
"""make tool file user id optional
Revision ID: e927d80409a1
Revises: a91b476a53de
Create Date: 2025-04-05 21:00:53.060052
"""
from alembic import op
import models as models
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'e927d80409a1'
down_revision = 'a91b476a53de'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('tool_files', schema=None) as batch_op:
batch_op.alter_column('user_id',
existing_type=sa.UUID(),
nullable=True)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table('tool_files', schema=None) as batch_op:
batch_op.alter_column('user_id',
existing_type=sa.UUID(),
nullable=False)
# ### end Alembic commands ###

@ -0,0 +1,25 @@
"""empty message
Revision ID: 81cd7d2f1cf0
Revises: e927d80409a1, 71afb1a6d8dc
Create Date: 2025-05-25 15:50:43.648214
"""
from alembic import op
import models as models
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '81cd7d2f1cf0'
down_revision = ('e927d80409a1', '71afb1a6d8dc')
branch_labels = None
depends_on = None
def upgrade():
pass
def downgrade():
pass

4782
api/poetry.lock generated

File diff suppressed because it is too large Load Diff

@ -87,6 +87,8 @@ dependencies = [
"webvtt-py~=0.5.1",
"alibabacloud-dysmsapi20170525>=4.1.0",
"flasgger>=0.9.7.1",
"weasyprint>=65.1",
"reportlab>=4.4.1",
]
# Before adding new dependency, consider place it in
# alphabet order (a-z) and suitable group.
@ -99,6 +101,8 @@ packages = []
############################################################
[tool.poetry.dependencies]
authlib = "1.3.1"
azure-identity = "1.16.1"
beautifulsoup4 = "4.12.2"
@ -157,6 +161,7 @@ starlette = "0.41.0"
tiktoken = "~0.8.0"
tokenizers = "~0.15.0"
transformers = "~4.35.0"
types-pytz = "~2024.2.0.20241003"
unstructured = { version = "~0.16.1", extras = [
"docx",
"epub",
@ -166,16 +171,21 @@ unstructured = { version = "~0.16.1", extras = [
"pptx",
] }
validators = "0.21.0"
volcengine-python-sdk = { extras = ["ark"], version = "~1.0.98" }
websocket-client = "~1.7.0"
xinference-client = "0.15.2"
yarl = "~1.18.3"
# Before adding new dependency, consider place it in alphabet order (a-z) and suitable group.
############################################################
flasgger = "^0.9.7.1"############################################################
# [ Indirect ] dependency group
# Related transparent dependencies with pinned version
# required by main implementations
############################################################
flasgger = "^0.9.7.1"
alibabacloud-dysmsapi20170525 = "^3.1.1"
weasyprint = "^65.1"
[tool.poetry.group.indirect.dependencies]
kaleido = "0.2.1"
rank-bm25 = "~0.2.2"
@ -186,10 +196,6 @@ package = false
[dependency-groups]
############################################################
# [ Dev ] dependency group
# Required for development and running tests
############################################################
dev = [
"coverage~=7.2.4",
"dotenv-linter~=0.5.0",
@ -240,6 +246,7 @@ dev = [
"types-ujson~=5.10.0",
]
############################################################
# [ Storage ] dependency group
# Required for storage clients

@ -620,7 +620,7 @@ name = "brotlicffi"
version = "1.1.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation == 'PyPy'" },
{ name = "cffi" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/9d/70caa61192f570fcf0352766331b735afa931b4c6bc9a348a0925cc13288/brotlicffi-1.1.0.0.tar.gz", hash = "sha256:b77827a689905143f87915310b93b273ab17888fd43ef350d4832c4a71083c13", size = 465192, upload-time = "2023-09-14T14:22:40.707Z" }
wheels = [
@ -1112,6 +1112,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/57/ff/f3b4b2d007c2a646b0f69440ab06224f9cf37a977a72cdb7b50632174e8a/cryptography-44.0.2-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:04abd71114848aa25edb28e225ab5f268096f44cf0127f3d36975bdf1bdf3390", size = 4107081, upload-time = "2025-03-02T00:01:28.938Z" },
]
[[package]]
name = "cssselect2"
version = "0.8.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "tinycss2" },
{ name = "webencodings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/9f/86/fd7f58fc498b3166f3a7e8e0cddb6e620fe1da35b02248b1bd59e95dbaaa/cssselect2-0.8.0.tar.gz", hash = "sha256:7674ffb954a3b46162392aee2a3a0aedb2e14ecf99fcc28644900f4e6e3e9d3a", size = 35716, upload-time = "2025-03-05T14:46:07.988Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/e7/aa315e6a749d9b96c2504a1ba0ba031ba2d0517e972ce22682e3fccecb09/cssselect2-0.8.0-py3-none-any.whl", hash = "sha256:46fc70ebc41ced7a32cd42d58b1884d72ade23d21e5a4eaaf022401c13f0e76e", size = 15454, upload-time = "2025-03-05T14:46:06.463Z" },
]
[[package]]
name = "dataclasses-json"
version = "0.6.7"
@ -1240,6 +1253,7 @@ dependencies = [
{ name = "pyyaml" },
{ name = "readabilipy" },
{ name = "redis", extra = ["hiredis"] },
{ name = "reportlab" },
{ name = "resend" },
{ name = "sentry-sdk", extra = ["flask"] },
{ name = "sqlalchemy" },
@ -1248,6 +1262,7 @@ dependencies = [
{ name = "tokenizers" },
{ name = "transformers" },
{ name = "unstructured", extra = ["docx", "epub", "md", "ppt", "pptx"] },
{ name = "weasyprint" },
{ name = "weave" },
{ name = "webvtt-py" },
{ name = "yarl" },
@ -1413,6 +1428,7 @@ requires-dist = [
{ name = "pyyaml", specifier = "~=6.0.1" },
{ name = "readabilipy", specifier = "==0.2.0" },
{ name = "redis", extras = ["hiredis"], specifier = "~=5.0.3" },
{ name = "reportlab", specifier = ">=4.4.1" },
{ name = "resend", specifier = "~=0.7.0" },
{ name = "sentry-sdk", extras = ["flask"], specifier = "~=1.44.1" },
{ name = "sqlalchemy", specifier = "~=2.0.29" },
@ -1421,6 +1437,7 @@ requires-dist = [
{ name = "tokenizers", specifier = "~=0.15.0" },
{ name = "transformers", specifier = "~=4.35.0" },
{ name = "unstructured", extras = ["docx", "epub", "md", "ppt", "pptx"], specifier = "~=0.16.1" },
{ name = "weasyprint", specifier = ">=65.1" },
{ name = "weave", specifier = "~=0.51.34" },
{ name = "webvtt-py", specifier = "~=0.5.1" },
{ name = "yarl", specifier = "~=1.18.3" },
@ -1816,6 +1833,38 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/25/155f9f080d5e4bc0082edfda032ea2bc2b8fab3f4d25d46c1e9dd22a1a89/flatbuffers-25.2.10-py2.py3-none-any.whl", hash = "sha256:ebba5f4d5ea615af3f7fd70fc310636fbb2bbd1f566ac0a23d98dd412de50051", size = 30953, upload-time = "2025-02-11T04:26:44.484Z" },
]
[[package]]
name = "fonttools"
version = "4.58.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9a/cf/4d037663e2a1fe30fddb655d755d76e18624be44ad467c07412c2319ab97/fonttools-4.58.0.tar.gz", hash = "sha256:27423d0606a2c7b336913254bf0b1193ebd471d5f725d665e875c5e88a011a43", size = 3514522, upload-time = "2025-05-10T17:36:35.886Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/2e/9b9bd943872a50cb182382f8f4a99af92d76e800603d5f73e4343fdce61a/fonttools-4.58.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9345b1bb994476d6034996b31891c0c728c1059c05daa59f9ab57d2a4dce0f84", size = 2751920, upload-time = "2025-05-10T17:35:16.487Z" },
{ url = "https://files.pythonhosted.org/packages/9b/8c/e8d6375da893125f610826c2e30e6d2597dfb8dad256f8ff5a54f3089fda/fonttools-4.58.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1d93119ace1e2d39ff1340deb71097932f72b21c054bd3da727a3859825e24e5", size = 2313957, upload-time = "2025-05-10T17:35:18.906Z" },
{ url = "https://files.pythonhosted.org/packages/4f/1b/a29cb00c8c20164b24f88780e298fafd0bbfb25cf8bc7b10c4b69331ad5d/fonttools-4.58.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79c9e4f01bb04f19df272ae35314eb6349fdb2e9497a163cd22a21be999694bd", size = 4913808, upload-time = "2025-05-10T17:35:21.394Z" },
{ url = "https://files.pythonhosted.org/packages/d1/ab/9b9507b65b15190cbfe1ccd3c08067d79268d8312ef20948b16d9f5aa905/fonttools-4.58.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:62ecda1465d38248aaf9bee1c17a21cf0b16aef7d121d7d303dbb320a6fd49c2", size = 4935876, upload-time = "2025-05-10T17:35:23.849Z" },
{ url = "https://files.pythonhosted.org/packages/15/e4/1395853bc775b0ab06a1c61cf261779afda7baff3f65cf1197bbd21aa149/fonttools-4.58.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:29d0499bff12a26733c05c1bfd07e68465158201624b2fba4a40b23d96c43f94", size = 4974798, upload-time = "2025-05-10T17:35:26.189Z" },
{ url = "https://files.pythonhosted.org/packages/3c/b9/0358368ef5462f4653a198207b29885bee8d5e23c870f6125450ed88e693/fonttools-4.58.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1871abdb0af582e2d96cc12d88889e3bfa796928f491ec14d34a2e58ca298c7e", size = 5093560, upload-time = "2025-05-10T17:35:28.577Z" },
{ url = "https://files.pythonhosted.org/packages/11/00/f64bc3659980c41eccf2c371e62eb15b40858f02a41a0e9c6258ef094388/fonttools-4.58.0-cp311-cp311-win32.whl", hash = "sha256:e292485d70402093eb94f6ab7669221743838b8bd4c1f45c84ca76b63338e7bf", size = 2186330, upload-time = "2025-05-10T17:35:31.733Z" },
{ url = "https://files.pythonhosted.org/packages/c8/a0/0287be13a1ec7733abf292ffbd76417cea78752d4ce10fecf92d8b1252d6/fonttools-4.58.0-cp311-cp311-win_amd64.whl", hash = "sha256:6df3755fcf9ad70a74ad3134bd5c9738f73c9bb701a304b1c809877b11fe701c", size = 2234687, upload-time = "2025-05-10T17:35:34.015Z" },
{ url = "https://files.pythonhosted.org/packages/6a/4e/1c6b35ec7c04d739df4cf5aace4b7ec284d6af2533a65de21972e2f237d9/fonttools-4.58.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:aa8316798f982c751d71f0025b372151ea36405733b62d0d94d5e7b8dd674fa6", size = 2737502, upload-time = "2025-05-10T17:35:36.436Z" },
{ url = "https://files.pythonhosted.org/packages/fc/72/c6fcafa3c9ed2b69991ae25a1ba7a3fec8bf74928a96e8229c37faa8eda2/fonttools-4.58.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c6db489511e867633b859b11aefe1b7c0d90281c5bdb903413edbb2ba77b97f1", size = 2307214, upload-time = "2025-05-10T17:35:38.939Z" },
{ url = "https://files.pythonhosted.org/packages/52/11/1015cedc9878da6d8d1758049749eef857b693e5828d477287a959c8650f/fonttools-4.58.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:107bdb2dacb1f627db3c4b77fb16d065a10fe88978d02b4fc327b9ecf8a62060", size = 4811136, upload-time = "2025-05-10T17:35:41.491Z" },
{ url = "https://files.pythonhosted.org/packages/32/b9/6a1bc1af6ec17eead5d32e87075e22d0dab001eace0b5a1542d38c6a9483/fonttools-4.58.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba7212068ab20f1128a0475f169068ba8e5b6e35a39ba1980b9f53f6ac9720ac", size = 4876598, upload-time = "2025-05-10T17:35:43.986Z" },
{ url = "https://files.pythonhosted.org/packages/d8/46/b14584c7ea65ad1609fb9632251016cda8a2cd66b15606753b9f888d3677/fonttools-4.58.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f95ea3b6a3b9962da3c82db73f46d6a6845a6c3f3f968f5293b3ac1864e771c2", size = 4872256, upload-time = "2025-05-10T17:35:46.617Z" },
{ url = "https://files.pythonhosted.org/packages/05/78/b2105a7812ca4ef9bf180cd741c82f4522316c652ce2a56f788e2eb54b62/fonttools-4.58.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:874f1225cc4ccfeac32009887f722d7f8b107ca5e867dcee067597eef9d4c80b", size = 5028710, upload-time = "2025-05-10T17:35:49.227Z" },
{ url = "https://files.pythonhosted.org/packages/8c/a9/a38c85ffd30d1f2c7a5460c8abfd1aa66e00c198df3ff0b08117f5c6fcd9/fonttools-4.58.0-cp312-cp312-win32.whl", hash = "sha256:5f3cde64ec99c43260e2e6c4fa70dfb0a5e2c1c1d27a4f4fe4618c16f6c9ff71", size = 2173593, upload-time = "2025-05-10T17:35:51.226Z" },
{ url = "https://files.pythonhosted.org/packages/66/48/29752962a74b7ed95da976b5a968bba1fe611a4a7e50b9fefa345e6e7025/fonttools-4.58.0-cp312-cp312-win_amd64.whl", hash = "sha256:2aee08e2818de45067109a207cbd1b3072939f77751ef05904d506111df5d824", size = 2223230, upload-time = "2025-05-10T17:35:53.653Z" },
{ url = "https://files.pythonhosted.org/packages/9b/1f/4417c26e26a1feab85a27e927f7a73d8aabc84544be8ba108ce4aa90eb1e/fonttools-4.58.0-py3-none-any.whl", hash = "sha256:c96c36880be2268be409df7b08c5b5dacac1827083461a6bc2cb07b8cbcec1d7", size = 1111440, upload-time = "2025-05-10T17:36:33.607Z" },
]
[package.optional-dependencies]
woff = [
{ name = "brotli", marker = "platform_python_implementation == 'CPython'" },
{ name = "brotlicffi", marker = "platform_python_implementation != 'CPython'" },
{ name = "zopfli" },
]
[[package]]
name = "frozenlist"
version = "1.5.0"
@ -4301,6 +4350,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5e/f9/ff95fd7d760af42f647ea87f9b8a383d891cdb5e5dbd4613edaeb094252a/pydantic_settings-2.6.1-py3-none-any.whl", hash = "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87", size = 28595, upload-time = "2024-11-01T11:00:02.64Z" },
]
[[package]]
name = "pydyf"
version = "0.11.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2e/c2/97fc6ce4ce0045080dc99446def812081b57750ed8aa67bfdfafa4561fe5/pydyf-0.11.0.tar.gz", hash = "sha256:394dddf619cca9d0c55715e3c55ea121a9bf9cbc780cdc1201a2427917b86b64", size = 17769, upload-time = "2024-07-12T12:26:51.95Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c9/ac/d5db977deaf28c6ecbc61bbca269eb3e8f0b3a1f55c8549e5333e606e005/pydyf-0.11.0-py3-none-any.whl", hash = "sha256:0aaf9e2ebbe786ec7a78ec3fbffa4cdcecde53fd6f563221d53c6bc1328848a3", size = 8104, upload-time = "2024-07-12T12:26:49.896Z" },
]
[[package]]
name = "pygments"
version = "2.19.1"
@ -4439,6 +4497,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/6b/2706497c86e8d69fb76afe5ea857fe1794621aa0f3b1d863feb953fe0f22/pypdfium2-4.30.1-py3-none-win_arm64.whl", hash = "sha256:c2b6d63f6d425d9416c08d2511822b54b8e3ac38e639fc41164b1d75584b3a8c", size = 2814810, upload-time = "2024-12-19T19:28:09.857Z" },
]
[[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]]
name = "pypika"
version = "0.48.9"
@ -4864,6 +4931,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/38/ec/ad2d7de49a600cdb8dd78434a1aeffe28b9d6fc42eb36afab4a27ad23384/regex-2024.11.6-cp312-cp312-win_amd64.whl", hash = "sha256:a93c194e2df18f7d264092dc8539b8ffb86b45b899ab976aa15d48214138e81b", size = 273567, upload-time = "2024-11-06T20:10:43.467Z" },
]
[[package]]
name = "reportlab"
version = "4.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "chardet" },
{ name = "pillow" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7b/d8/c3366bf10a5a5fcc3467eefa9504f6aa24fcda5817b5b147eabd37a385e1/reportlab-4.4.1.tar.gz", hash = "sha256:5f9b9fc0b7a48e8912c25ccf69d26b82980ab0da718e4f583fa720e8f8f5073f", size = 3509107, upload-time = "2025-05-15T08:07:02.539Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/2e/7994a139150abf11c8dd258feb091ad654823a83cfd9720bfdded27185c3/reportlab-4.4.1-py3-none-any.whl", hash = "sha256:9217a1c8c1917217f819718b24972a96ad0c485a1c494749562d097b58d974b7", size = 1953615, upload-time = "2025-05-15T08:06:59.248Z" },
]
[[package]]
name = "requests"
version = "2.31.0"
@ -5403,6 +5483,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/64/b16003419a1d7728d0d8c0d56a4c24325e7b10a21a9dd1fc0f7115c02f0a/tiktoken-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:5a62d7a25225bafed786a524c1b9f0910a1128f4232615bf3f8257a73aaa3b16", size = 894897, upload-time = "2025-02-14T06:02:36.265Z" },
]
[[package]]
name = "tinycss2"
version = "1.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "webencodings" },
]
sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" },
]
[[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]]
name = "tokenizers"
version = "0.15.2"
@ -6253,6 +6357,25 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" },
]
[[package]]
name = "weasyprint"
version = "65.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/38/76/7f865f0019120be20276813097b5729b8487b93dd4aff339aa77ed8c7ad2/weasyprint-65.1.tar.gz", hash = "sha256:120281bdbd42ffaa7d7e5cedbe3182a2cef36ea5ad97fe9f357e43be6a1e58ea", size = 499028, upload-time = "2025-04-14T12:15:02.654Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fe/9a/14f4e5fd4bba988d3684602b72f04c0b299c0f368d26c11a79ceab97aa68/weasyprint-65.1-py3-none-any.whl", hash = "sha256:9baa54282dc86929f6b877034d06b0416e2a7cacb1af3f73d80960592fd0af89", size = 298040, upload-time = "2025-04-14T12:15:00.695Z" },
]
[[package]]
name = "weave"
version = "0.51.43"
@ -6521,6 +6644,34 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/c7/3c67562e03b3752ba4ab6b23355f15a58ac2d023a6ef763caaca430f91f2/zope.interface-7.2-cp312-cp312-win_amd64.whl", hash = "sha256:29caad142a2355ce7cfea48725aa8bcf0067e2b5cc63fcf5cd9f97ad12d6afb5", size = 212466, upload-time = "2024-11-28T08:49:14.397Z" },
]
[[package]]
name = "zopfli"
version = "0.2.3.post1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5e/7c/a8f6696e694709e2abcbccd27d05ef761e9b6efae217e11d977471555b62/zopfli-0.2.3.post1.tar.gz", hash = "sha256:96484dc0f48be1c5d7ae9f38ed1ce41e3675fd506b27c11a6607f14b49101e99", size = 175629, upload-time = "2024-10-18T15:42:05.946Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/92/6d/c8224a8fc77c1dff6caaa2dc63794a40ea284c82ac20030fb2521092dca6/zopfli-0.2.3.post1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:518f1f4ed35dd69ce06b552f84e6d081f07c552b4c661c5312d950a0b764a58a", size = 296334, upload-time = "2024-10-18T15:40:44.684Z" },
{ url = "https://files.pythonhosted.org/packages/f8/da/df0f87a489d223f184d69e9e88c80c1314be43b2361acffefdc09659e00d/zopfli-0.2.3.post1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:615a8ac9dda265e9cc38b2a76c3142e4a9f30fea4a79c85f670850783bc6feb4", size = 163886, upload-time = "2024-10-18T15:40:45.812Z" },
{ url = "https://files.pythonhosted.org/packages/39/b7/14529a7ae608cedddb2f791cbc13a392a246e2e6d9c9b4b8bcda707d08d8/zopfli-0.2.3.post1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a82fc2dbebe6eb908b9c665e71496f8525c1bc4d2e3a7a7722ef2b128b6227c8", size = 823654, upload-time = "2024-10-18T15:40:46.969Z" },
{ url = "https://files.pythonhosted.org/packages/57/48/217c7bd720553d9e68b96926c02820e8b6184ef6dbac937823abad85b154/zopfli-0.2.3.post1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37d011e92f7b9622742c905fdbed9920a1d0361df84142807ea2a528419dea7f", size = 826188, upload-time = "2024-10-18T15:40:48.147Z" },
{ url = "https://files.pythonhosted.org/packages/2f/8b/5ab8c4c6db2564a0c3369e584090c101ffad4f9d0a39396e0d3e80c98413/zopfli-0.2.3.post1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e63d558847166543c2c9789e6f985400a520b7eacc4b99181668b2c3aeadd352", size = 850573, upload-time = "2024-10-18T15:40:49.481Z" },
{ url = "https://files.pythonhosted.org/packages/33/f8/f52ec5c713f3325c852f19af7c8e3f98109ddcd1ce400dc39005072a2fea/zopfli-0.2.3.post1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:60db20f06c3d4c5934b16cfa62a2cc5c3f0686bffe0071ed7804d3c31ab1a04e", size = 1754164, upload-time = "2024-10-18T15:40:50.952Z" },
{ url = "https://files.pythonhosted.org/packages/92/24/6a6018125e1cc6ee5880a0ae60456fdc8a2da43f2f14b487cf49439a3448/zopfli-0.2.3.post1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:716cdbfc57bfd3d3e31a58e6246e8190e6849b7dbb7c4ce39ef8bbf0edb8f6d5", size = 1906135, upload-time = "2024-10-18T15:40:52.484Z" },
{ url = "https://files.pythonhosted.org/packages/87/ad/697521dac8b46f0e0d081a3da153687d7583f3a2cd5466af1ddb9928394f/zopfli-0.2.3.post1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3a89277ed5f8c0fb2d0b46d669aa0633123aa7381f1f6118c12f15e0fb48f8ca", size = 1835047, upload-time = "2024-10-18T15:40:54.453Z" },
{ url = "https://files.pythonhosted.org/packages/95/00/042c0cdba957343d7a83e572fc5ffe62de03d57c43075c8cf920b8b542e6/zopfli-0.2.3.post1-cp311-cp311-win32.whl", hash = "sha256:75a26a2307b10745a83b660c404416e984ee6fca515ec7f0765f69af3ce08072", size = 82635, upload-time = "2024-10-18T15:40:55.632Z" },
{ url = "https://files.pythonhosted.org/packages/e6/cc/07119cba00db12d7ef0472637b7d71a95f2c8e9a20ed460d759acd274887/zopfli-0.2.3.post1-cp311-cp311-win_amd64.whl", hash = "sha256:81c341d9bb87a6dbbb0d45d6e272aca80c7c97b4b210f9b6e233bf8b87242f29", size = 99345, upload-time = "2024-10-18T15:40:56.965Z" },
{ url = "https://files.pythonhosted.org/packages/3f/ce/b6441cc01881d06e0b5883f32c44e7cc9772e0d04e3e59277f59f80b9a19/zopfli-0.2.3.post1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3f0197b6aa6eb3086ae9e66d6dd86c4d502b6c68b0ec490496348ae8c05ecaef", size = 295489, upload-time = "2024-10-18T15:40:57.96Z" },
{ url = "https://files.pythonhosted.org/packages/93/f0/24dd708f00ae0a925bc5c9edae858641c80f6a81a516810dc4d21688a930/zopfli-0.2.3.post1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fcfc0dc2761e4fcc15ad5d273b4d58c2e8e059d3214a7390d4d3c8e2aee644e", size = 163010, upload-time = "2024-10-18T15:40:59.444Z" },
{ url = "https://files.pythonhosted.org/packages/65/57/0378eeeb5e3e1e83b1b0958616b2bf954f102ba5b0755b9747dafbd8cb72/zopfli-0.2.3.post1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cac2b37ab21c2b36a10b685b1893ebd6b0f83ae26004838ac817680881576567", size = 823649, upload-time = "2024-10-18T15:41:00.642Z" },
{ url = "https://files.pythonhosted.org/packages/ab/8a/3ab8a616d4655acf5cf63c40ca84e434289d7d95518a1a42d28b4a7228f8/zopfli-0.2.3.post1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d5ab297d660b75c159190ce6d73035502310e40fd35170aed7d1a1aea7ddd65", size = 826557, upload-time = "2024-10-18T15:41:02.431Z" },
{ url = "https://files.pythonhosted.org/packages/ed/4d/7f6820af119c4fec6efaf007bffee7bc9052f695853a711a951be7afd26b/zopfli-0.2.3.post1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ba214f4f45bec195ee8559651154d3ac2932470b9d91c5715fc29c013349f8c", size = 851127, upload-time = "2024-10-18T15:41:04.259Z" },
{ url = "https://files.pythonhosted.org/packages/e1/db/1ef5353ab06f9f2fb0c25ed0cddf1418fe275cc2ee548bc4a29340c44fe1/zopfli-0.2.3.post1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1e0ed5d84ffa2d677cc9582fc01e61dab2e7ef8b8996e055f0a76167b1b94df", size = 1754183, upload-time = "2024-10-18T15:41:05.808Z" },
{ url = "https://files.pythonhosted.org/packages/39/03/44f8f39950354d330fa798e4bab1ac8e38ec787d3fde25d5b9c7770065a2/zopfli-0.2.3.post1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bfa1eb759e07d8b7aa7a310a2bc535e127ee70addf90dc8d4b946b593c3e51a8", size = 1905945, upload-time = "2024-10-18T15:41:07.136Z" },
{ url = "https://files.pythonhosted.org/packages/74/7b/94b920c33cc64255f59e3cfc77c829b5c6e60805d189baeada728854a342/zopfli-0.2.3.post1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cd2c002f160502608dcc822ed2441a0f4509c52e86fcfd1a09e937278ed1ca14", size = 1835885, upload-time = "2024-10-18T15:41:08.705Z" },
{ url = "https://files.pythonhosted.org/packages/ad/89/c869ac844351e285a6165e2da79b715b0619a122e3160d183805adf8ab45/zopfli-0.2.3.post1-cp312-cp312-win32.whl", hash = "sha256:7be5cc6732eb7b4df17305d8a7b293223f934a31783a874a01164703bc1be6cd", size = 82743, upload-time = "2024-10-18T15:41:10.377Z" },
{ url = "https://files.pythonhosted.org/packages/29/e6/c98912fd3a589d8a7316c408fd91519f72c237805c4400b753e3942fda0b/zopfli-0.2.3.post1-cp312-cp312-win_amd64.whl", hash = "sha256:4e50ffac74842c1c1018b9b73875a0d0a877c066ab06bf7cccbaa84af97e754f", size = 99403, upload-time = "2024-10-18T15:41:11.547Z" },
]
[[package]]
name = "zstandard"
version = "0.23.0"

Loading…
Cancel
Save