dev #3
2
.gitignore
vendored
2
.gitignore
vendored
@@ -41,6 +41,8 @@ MANIFEST
|
|||||||
venv/
|
venv/
|
||||||
ENV/
|
ENV/
|
||||||
env/
|
env/
|
||||||
|
.uv/
|
||||||
|
.uv-cache/
|
||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
*.db
|
*.db
|
||||||
*.sqlite
|
*.sqlite
|
||||||
|
|||||||
1
.python-version
Normal file
1
.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.14
|
||||||
@@ -7,6 +7,8 @@ import json
|
|||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
|
|
||||||
|
from app.core.collected_data_fields import get_metadata_field
|
||||||
|
from app.core.countries import COUNTRY_OPTIONS, get_country_search_variants, normalize_country
|
||||||
from app.db.session import get_db
|
from app.db.session import get_db
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.core.security import get_current_user
|
from app.core.security import get_current_user
|
||||||
@@ -15,8 +17,119 @@ from app.models.collected_data import CollectedData
|
|||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
COUNTRY_SQL = "metadata->>'country'"
|
||||||
|
SEARCHABLE_SQL = [
|
||||||
|
"name",
|
||||||
|
"title",
|
||||||
|
"description",
|
||||||
|
"source",
|
||||||
|
"data_type",
|
||||||
|
"source_id",
|
||||||
|
"metadata::text",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def parse_multi_values(value: Optional[str]) -> list[str]:
|
||||||
|
if not value:
|
||||||
|
return []
|
||||||
|
return [item.strip() for item in value.split(",") if item.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def build_in_condition(field_sql: str, values: list[str], param_prefix: str, params: dict) -> str:
|
||||||
|
placeholders = []
|
||||||
|
for index, value in enumerate(values):
|
||||||
|
key = f"{param_prefix}_{index}"
|
||||||
|
params[key] = value
|
||||||
|
placeholders.append(f":{key}")
|
||||||
|
return f"{field_sql} IN ({', '.join(placeholders)})"
|
||||||
|
|
||||||
|
|
||||||
|
def build_search_condition(search: Optional[str], params: dict) -> Optional[str]:
|
||||||
|
if not search:
|
||||||
|
return None
|
||||||
|
|
||||||
|
normalized = search.strip()
|
||||||
|
if not normalized:
|
||||||
|
return None
|
||||||
|
|
||||||
|
search_terms = [normalized]
|
||||||
|
for variant in get_country_search_variants(normalized):
|
||||||
|
if variant.casefold() not in {term.casefold() for term in search_terms}:
|
||||||
|
search_terms.append(variant)
|
||||||
|
|
||||||
|
conditions = []
|
||||||
|
for index, term in enumerate(search_terms):
|
||||||
|
params[f"search_{index}"] = f"%{term}%"
|
||||||
|
conditions.extend(f"{field} ILIKE :search_{index}" for field in SEARCHABLE_SQL)
|
||||||
|
|
||||||
|
params["search_exact"] = normalized
|
||||||
|
params["search_prefix"] = f"{normalized}%"
|
||||||
|
|
||||||
|
canonical_variants = get_country_search_variants(normalized)
|
||||||
|
canonical = canonical_variants[0] if canonical_variants else None
|
||||||
|
params["country_search_exact"] = canonical or normalized
|
||||||
|
params["country_search_prefix"] = f"{(canonical or normalized)}%"
|
||||||
|
|
||||||
|
return "(" + " OR ".join(conditions) + ")"
|
||||||
|
|
||||||
|
|
||||||
|
def build_search_rank_sql(search: Optional[str]) -> str:
|
||||||
|
if not search or not search.strip():
|
||||||
|
return "0"
|
||||||
|
|
||||||
|
return """
|
||||||
|
CASE
|
||||||
|
WHEN name ILIKE :search_exact THEN 700
|
||||||
|
WHEN name ILIKE :search_prefix THEN 600
|
||||||
|
WHEN title ILIKE :search_exact THEN 500
|
||||||
|
WHEN title ILIKE :search_prefix THEN 400
|
||||||
|
WHEN metadata->>'country' ILIKE :country_search_exact THEN 380
|
||||||
|
WHEN metadata->>'country' ILIKE :country_search_prefix THEN 340
|
||||||
|
WHEN source_id ILIKE :search_exact THEN 350
|
||||||
|
WHEN source ILIKE :search_exact THEN 300
|
||||||
|
WHEN data_type ILIKE :search_exact THEN 250
|
||||||
|
WHEN description ILIKE :search_0 THEN 150
|
||||||
|
WHEN metadata::text ILIKE :search_0 THEN 100
|
||||||
|
WHEN title ILIKE :search_0 THEN 80
|
||||||
|
WHEN name ILIKE :search_0 THEN 60
|
||||||
|
WHEN source ILIKE :search_0 THEN 40
|
||||||
|
WHEN data_type ILIKE :search_0 THEN 30
|
||||||
|
WHEN source_id ILIKE :search_0 THEN 20
|
||||||
|
ELSE 0
|
||||||
|
END
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_collected_row(row) -> dict:
|
||||||
|
metadata = row[7]
|
||||||
|
return {
|
||||||
|
"id": row[0],
|
||||||
|
"source": row[1],
|
||||||
|
"source_id": row[2],
|
||||||
|
"data_type": row[3],
|
||||||
|
"name": row[4],
|
||||||
|
"title": row[5],
|
||||||
|
"description": row[6],
|
||||||
|
"country": get_metadata_field(metadata, "country"),
|
||||||
|
"city": get_metadata_field(metadata, "city"),
|
||||||
|
"latitude": get_metadata_field(metadata, "latitude"),
|
||||||
|
"longitude": get_metadata_field(metadata, "longitude"),
|
||||||
|
"value": get_metadata_field(metadata, "value"),
|
||||||
|
"unit": get_metadata_field(metadata, "unit"),
|
||||||
|
"metadata": metadata,
|
||||||
|
"cores": get_metadata_field(metadata, "cores"),
|
||||||
|
"rmax": get_metadata_field(metadata, "rmax"),
|
||||||
|
"rpeak": get_metadata_field(metadata, "rpeak"),
|
||||||
|
"power": get_metadata_field(metadata, "power"),
|
||||||
|
"collected_at": row[8].isoformat() if row[8] else None,
|
||||||
|
"reference_date": row[9].isoformat() if row[9] else None,
|
||||||
|
"is_valid": row[10],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
async def list_collected_data(
|
async def list_collected_data(
|
||||||
|
mode: str = Query("current", description="查询模式: current/history"),
|
||||||
source: Optional[str] = Query(None, description="数据源过滤"),
|
source: Optional[str] = Query(None, description="数据源过滤"),
|
||||||
data_type: Optional[str] = Query(None, description="数据类型过滤"),
|
data_type: Optional[str] = Query(None, description="数据类型过滤"),
|
||||||
country: Optional[str] = Query(None, description="国家过滤"),
|
country: Optional[str] = Query(None, description="国家过滤"),
|
||||||
@@ -27,25 +140,30 @@ async def list_collected_data(
|
|||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""查询采集的数据列表"""
|
"""查询采集的数据列表"""
|
||||||
|
normalized_country = normalize_country(country) if country else None
|
||||||
|
source_values = parse_multi_values(source)
|
||||||
|
data_type_values = parse_multi_values(data_type)
|
||||||
|
|
||||||
# Build WHERE clause
|
# Build WHERE clause
|
||||||
conditions = []
|
conditions = []
|
||||||
params = {}
|
params = {}
|
||||||
|
|
||||||
if source:
|
if mode != "history":
|
||||||
conditions.append("source = :source")
|
conditions.append("COALESCE(is_current, TRUE) = TRUE")
|
||||||
params["source"] = source
|
|
||||||
if data_type:
|
if source_values:
|
||||||
conditions.append("data_type = :data_type")
|
conditions.append(build_in_condition("source", source_values, "source", params))
|
||||||
params["data_type"] = data_type
|
if data_type_values:
|
||||||
if country:
|
conditions.append(build_in_condition("data_type", data_type_values, "data_type", params))
|
||||||
conditions.append("country = :country")
|
if normalized_country:
|
||||||
params["country"] = country
|
conditions.append(f"{COUNTRY_SQL} = :country")
|
||||||
if search:
|
params["country"] = normalized_country
|
||||||
conditions.append("(name ILIKE :search OR title ILIKE :search)")
|
search_condition = build_search_condition(search, params)
|
||||||
params["search"] = f"%{search}%"
|
if search_condition:
|
||||||
|
conditions.append(search_condition)
|
||||||
|
|
||||||
where_sql = " AND ".join(conditions) if conditions else "1=1"
|
where_sql = " AND ".join(conditions) if conditions else "1=1"
|
||||||
|
search_rank_sql = build_search_rank_sql(search)
|
||||||
|
|
||||||
# Calculate offset
|
# Calculate offset
|
||||||
offset = (page - 1) * page_size
|
offset = (page - 1) * page_size
|
||||||
@@ -58,11 +176,11 @@ async def list_collected_data(
|
|||||||
# Query data
|
# Query data
|
||||||
query = text(f"""
|
query = text(f"""
|
||||||
SELECT id, source, source_id, data_type, name, title, description,
|
SELECT id, source, source_id, data_type, name, title, description,
|
||||||
country, city, latitude, longitude, value, unit,
|
metadata, collected_at, reference_date, is_valid,
|
||||||
metadata, collected_at, reference_date, is_valid
|
{search_rank_sql} AS search_rank
|
||||||
FROM collected_data
|
FROM collected_data
|
||||||
WHERE {where_sql}
|
WHERE {where_sql}
|
||||||
ORDER BY collected_at DESC
|
ORDER BY search_rank DESC, collected_at DESC
|
||||||
LIMIT :limit OFFSET :offset
|
LIMIT :limit OFFSET :offset
|
||||||
""")
|
""")
|
||||||
params["limit"] = page_size
|
params["limit"] = page_size
|
||||||
@@ -73,27 +191,7 @@ async def list_collected_data(
|
|||||||
|
|
||||||
data = []
|
data = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
data.append(
|
data.append(serialize_collected_row(row[:11]))
|
||||||
{
|
|
||||||
"id": row[0],
|
|
||||||
"source": row[1],
|
|
||||||
"source_id": row[2],
|
|
||||||
"data_type": row[3],
|
|
||||||
"name": row[4],
|
|
||||||
"title": row[5],
|
|
||||||
"description": row[6],
|
|
||||||
"country": row[7],
|
|
||||||
"city": row[8],
|
|
||||||
"latitude": row[9],
|
|
||||||
"longitude": row[10],
|
|
||||||
"value": row[11],
|
|
||||||
"unit": row[12],
|
|
||||||
"metadata": row[13],
|
|
||||||
"collected_at": row[14].isoformat() if row[14] else None,
|
|
||||||
"reference_date": row[15].isoformat() if row[15] else None,
|
|
||||||
"is_valid": row[16],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"total": total,
|
"total": total,
|
||||||
@@ -105,16 +203,19 @@ async def list_collected_data(
|
|||||||
|
|
||||||
@router.get("/summary")
|
@router.get("/summary")
|
||||||
async def get_data_summary(
|
async def get_data_summary(
|
||||||
|
mode: str = Query("current", description="查询模式: current/history"),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""获取数据汇总统计"""
|
"""获取数据汇总统计"""
|
||||||
|
where_sql = "WHERE COALESCE(is_current, TRUE) = TRUE" if mode != "history" else ""
|
||||||
|
|
||||||
# By source and data_type
|
# By source and data_type
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
text("""
|
text("""
|
||||||
SELECT source, data_type, COUNT(*) as count
|
SELECT source, data_type, COUNT(*) as count
|
||||||
FROM collected_data
|
FROM collected_data
|
||||||
|
""" + where_sql + """
|
||||||
GROUP BY source, data_type
|
GROUP BY source, data_type
|
||||||
ORDER BY source, data_type
|
ORDER BY source, data_type
|
||||||
""")
|
""")
|
||||||
@@ -138,6 +239,7 @@ async def get_data_summary(
|
|||||||
text("""
|
text("""
|
||||||
SELECT source, COUNT(*) as count
|
SELECT source, COUNT(*) as count
|
||||||
FROM collected_data
|
FROM collected_data
|
||||||
|
""" + where_sql + """
|
||||||
GROUP BY source
|
GROUP BY source
|
||||||
ORDER BY count DESC
|
ORDER BY count DESC
|
||||||
""")
|
""")
|
||||||
@@ -153,6 +255,7 @@ async def get_data_summary(
|
|||||||
|
|
||||||
@router.get("/sources")
|
@router.get("/sources")
|
||||||
async def get_data_sources(
|
async def get_data_sources(
|
||||||
|
mode: str = Query("current", description="查询模式: current/history"),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
@@ -160,7 +263,9 @@ async def get_data_sources(
|
|||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
text("""
|
text("""
|
||||||
SELECT DISTINCT source FROM collected_data ORDER BY source
|
SELECT DISTINCT source FROM collected_data
|
||||||
|
""" + ("WHERE COALESCE(is_current, TRUE) = TRUE " if mode != "history" else "") + """
|
||||||
|
ORDER BY source
|
||||||
""")
|
""")
|
||||||
)
|
)
|
||||||
rows = result.fetchall()
|
rows = result.fetchall()
|
||||||
@@ -172,6 +277,7 @@ async def get_data_sources(
|
|||||||
|
|
||||||
@router.get("/types")
|
@router.get("/types")
|
||||||
async def get_data_types(
|
async def get_data_types(
|
||||||
|
mode: str = Query("current", description="查询模式: current/history"),
|
||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
@@ -179,7 +285,9 @@ async def get_data_types(
|
|||||||
|
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
text("""
|
text("""
|
||||||
SELECT DISTINCT data_type FROM collected_data ORDER BY data_type
|
SELECT DISTINCT data_type FROM collected_data
|
||||||
|
""" + ("WHERE COALESCE(is_current, TRUE) = TRUE " if mode != "history" else "") + """
|
||||||
|
ORDER BY data_type
|
||||||
""")
|
""")
|
||||||
)
|
)
|
||||||
rows = result.fetchall()
|
rows = result.fetchall()
|
||||||
@@ -196,17 +304,8 @@ async def get_countries(
|
|||||||
):
|
):
|
||||||
"""获取所有国家列表"""
|
"""获取所有国家列表"""
|
||||||
|
|
||||||
result = await db.execute(
|
|
||||||
text("""
|
|
||||||
SELECT DISTINCT country FROM collected_data
|
|
||||||
WHERE country IS NOT NULL AND country != ''
|
|
||||||
ORDER BY country
|
|
||||||
""")
|
|
||||||
)
|
|
||||||
rows = result.fetchall()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"countries": [row[0] for row in rows],
|
"countries": COUNTRY_OPTIONS,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -221,7 +320,6 @@ async def get_collected_data(
|
|||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
text("""
|
text("""
|
||||||
SELECT id, source, source_id, data_type, name, title, description,
|
SELECT id, source, source_id, data_type, name, title, description,
|
||||||
country, city, latitude, longitude, value, unit,
|
|
||||||
metadata, collected_at, reference_date, is_valid
|
metadata, collected_at, reference_date, is_valid
|
||||||
FROM collected_data
|
FROM collected_data
|
||||||
WHERE id = :id
|
WHERE id = :id
|
||||||
@@ -236,25 +334,7 @@ async def get_collected_data(
|
|||||||
detail="数据不存在",
|
detail="数据不存在",
|
||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return serialize_collected_row(row)
|
||||||
"id": row[0],
|
|
||||||
"source": row[1],
|
|
||||||
"source_id": row[2],
|
|
||||||
"data_type": row[3],
|
|
||||||
"name": row[4],
|
|
||||||
"title": row[5],
|
|
||||||
"description": row[6],
|
|
||||||
"country": row[7],
|
|
||||||
"city": row[8],
|
|
||||||
"latitude": row[9],
|
|
||||||
"longitude": row[10],
|
|
||||||
"value": row[11],
|
|
||||||
"unit": row[12],
|
|
||||||
"metadata": row[13],
|
|
||||||
"collected_at": row[14].isoformat() if row[14] else None,
|
|
||||||
"reference_date": row[15].isoformat() if row[15] else None,
|
|
||||||
"is_valid": row[16],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def build_where_clause(
|
def build_where_clause(
|
||||||
@@ -263,19 +343,21 @@ def build_where_clause(
|
|||||||
"""Build WHERE clause and params for queries"""
|
"""Build WHERE clause and params for queries"""
|
||||||
conditions = []
|
conditions = []
|
||||||
params = {}
|
params = {}
|
||||||
|
source_values = parse_multi_values(source)
|
||||||
|
data_type_values = parse_multi_values(data_type)
|
||||||
|
|
||||||
if source:
|
if source_values:
|
||||||
conditions.append("source = :source")
|
conditions.append(build_in_condition("source", source_values, "source", params))
|
||||||
params["source"] = source
|
if data_type_values:
|
||||||
if data_type:
|
conditions.append(build_in_condition("data_type", data_type_values, "data_type", params))
|
||||||
conditions.append("data_type = :data_type")
|
normalized_country = normalize_country(country) if country else None
|
||||||
params["data_type"] = data_type
|
|
||||||
if country:
|
if normalized_country:
|
||||||
conditions.append("country = :country")
|
conditions.append(f"{COUNTRY_SQL} = :country")
|
||||||
params["country"] = country
|
params["country"] = normalized_country
|
||||||
if search:
|
search_condition = build_search_condition(search, params)
|
||||||
conditions.append("(name ILIKE :search OR title ILIKE :search)")
|
if search_condition:
|
||||||
params["search"] = f"%{search}%"
|
conditions.append(search_condition)
|
||||||
|
|
||||||
where_sql = " AND ".join(conditions) if conditions else "1=1"
|
where_sql = " AND ".join(conditions) if conditions else "1=1"
|
||||||
return where_sql, params
|
return where_sql, params
|
||||||
@@ -283,6 +365,7 @@ def build_where_clause(
|
|||||||
|
|
||||||
@router.get("/export/json")
|
@router.get("/export/json")
|
||||||
async def export_json(
|
async def export_json(
|
||||||
|
mode: str = Query("current", description="查询模式: current/history"),
|
||||||
source: Optional[str] = Query(None, description="数据源过滤"),
|
source: Optional[str] = Query(None, description="数据源过滤"),
|
||||||
data_type: Optional[str] = Query(None, description="数据类型过滤"),
|
data_type: Optional[str] = Query(None, description="数据类型过滤"),
|
||||||
country: Optional[str] = Query(None, description="国家过滤"),
|
country: Optional[str] = Query(None, description="国家过滤"),
|
||||||
@@ -294,11 +377,12 @@ async def export_json(
|
|||||||
"""导出数据为 JSON 格式"""
|
"""导出数据为 JSON 格式"""
|
||||||
|
|
||||||
where_sql, params = build_where_clause(source, data_type, country, search)
|
where_sql, params = build_where_clause(source, data_type, country, search)
|
||||||
|
if mode != "history":
|
||||||
|
where_sql = f"({where_sql}) AND COALESCE(is_current, TRUE) = TRUE"
|
||||||
params["limit"] = limit
|
params["limit"] = limit
|
||||||
|
|
||||||
query = text(f"""
|
query = text(f"""
|
||||||
SELECT id, source, source_id, data_type, name, title, description,
|
SELECT id, source, source_id, data_type, name, title, description,
|
||||||
country, city, latitude, longitude, value, unit,
|
|
||||||
metadata, collected_at, reference_date, is_valid
|
metadata, collected_at, reference_date, is_valid
|
||||||
FROM collected_data
|
FROM collected_data
|
||||||
WHERE {where_sql}
|
WHERE {where_sql}
|
||||||
@@ -311,27 +395,7 @@ async def export_json(
|
|||||||
|
|
||||||
data = []
|
data = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
data.append(
|
data.append(serialize_collected_row(row))
|
||||||
{
|
|
||||||
"id": row[0],
|
|
||||||
"source": row[1],
|
|
||||||
"source_id": row[2],
|
|
||||||
"data_type": row[3],
|
|
||||||
"name": row[4],
|
|
||||||
"title": row[5],
|
|
||||||
"description": row[6],
|
|
||||||
"country": row[7],
|
|
||||||
"city": row[8],
|
|
||||||
"latitude": row[9],
|
|
||||||
"longitude": row[10],
|
|
||||||
"value": row[11],
|
|
||||||
"unit": row[12],
|
|
||||||
"metadata": row[13],
|
|
||||||
"collected_at": row[14].isoformat() if row[14] else None,
|
|
||||||
"reference_date": row[15].isoformat() if row[15] else None,
|
|
||||||
"is_valid": row[16],
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
json_str = json.dumps({"data": data, "total": len(data)}, ensure_ascii=False, indent=2)
|
json_str = json.dumps({"data": data, "total": len(data)}, ensure_ascii=False, indent=2)
|
||||||
|
|
||||||
@@ -346,6 +410,7 @@ async def export_json(
|
|||||||
|
|
||||||
@router.get("/export/csv")
|
@router.get("/export/csv")
|
||||||
async def export_csv(
|
async def export_csv(
|
||||||
|
mode: str = Query("current", description="查询模式: current/history"),
|
||||||
source: Optional[str] = Query(None, description="数据源过滤"),
|
source: Optional[str] = Query(None, description="数据源过滤"),
|
||||||
data_type: Optional[str] = Query(None, description="数据类型过滤"),
|
data_type: Optional[str] = Query(None, description="数据类型过滤"),
|
||||||
country: Optional[str] = Query(None, description="国家过滤"),
|
country: Optional[str] = Query(None, description="国家过滤"),
|
||||||
@@ -357,11 +422,12 @@ async def export_csv(
|
|||||||
"""导出数据为 CSV 格式"""
|
"""导出数据为 CSV 格式"""
|
||||||
|
|
||||||
where_sql, params = build_where_clause(source, data_type, country, search)
|
where_sql, params = build_where_clause(source, data_type, country, search)
|
||||||
|
if mode != "history":
|
||||||
|
where_sql = f"({where_sql}) AND COALESCE(is_current, TRUE) = TRUE"
|
||||||
params["limit"] = limit
|
params["limit"] = limit
|
||||||
|
|
||||||
query = text(f"""
|
query = text(f"""
|
||||||
SELECT id, source, source_id, data_type, name, title, description,
|
SELECT id, source, source_id, data_type, name, title, description,
|
||||||
country, city, latitude, longitude, value, unit,
|
|
||||||
metadata, collected_at, reference_date, is_valid
|
metadata, collected_at, reference_date, is_valid
|
||||||
FROM collected_data
|
FROM collected_data
|
||||||
WHERE {where_sql}
|
WHERE {where_sql}
|
||||||
@@ -409,16 +475,16 @@ async def export_csv(
|
|||||||
row[4],
|
row[4],
|
||||||
row[5],
|
row[5],
|
||||||
row[6],
|
row[6],
|
||||||
row[7],
|
get_metadata_field(row[7], "country"),
|
||||||
row[8],
|
get_metadata_field(row[7], "city"),
|
||||||
row[9],
|
get_metadata_field(row[7], "latitude"),
|
||||||
|
get_metadata_field(row[7], "longitude"),
|
||||||
|
get_metadata_field(row[7], "value"),
|
||||||
|
get_metadata_field(row[7], "unit"),
|
||||||
|
json.dumps(row[7]) if row[7] else "",
|
||||||
|
row[8].isoformat() if row[8] else "",
|
||||||
|
row[9].isoformat() if row[9] else "",
|
||||||
row[10],
|
row[10],
|
||||||
row[11],
|
|
||||||
row[12],
|
|
||||||
json.dumps(row[13]) if row[13] else "",
|
|
||||||
row[14].isoformat() if row[14] else "",
|
|
||||||
row[15].isoformat() if row[15] else "",
|
|
||||||
row[16],
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ from sqlalchemy import func, select
|
|||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
from app.core.security import get_current_user
|
from app.core.security import get_current_user
|
||||||
|
from app.core.data_sources import get_data_sources_config
|
||||||
from app.db.session import get_db
|
from app.db.session import get_db
|
||||||
from app.models.collected_data import CollectedData
|
from app.models.collected_data import CollectedData
|
||||||
from app.models.datasource import DataSource
|
from app.models.datasource import DataSource
|
||||||
from app.models.task import CollectionTask
|
from app.models.task import CollectionTask
|
||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.services.scheduler import run_collector_now, sync_datasource_job
|
from app.services.scheduler import get_latest_task_id_for_datasource, run_collector_now, sync_datasource_job
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -83,9 +84,11 @@ async def list_datasources(
|
|||||||
datasources = result.scalars().all()
|
datasources = result.scalars().all()
|
||||||
|
|
||||||
collector_list = []
|
collector_list = []
|
||||||
|
config = get_data_sources_config()
|
||||||
for datasource in datasources:
|
for datasource in datasources:
|
||||||
running_task = await get_running_task(db, datasource.id)
|
running_task = await get_running_task(db, datasource.id)
|
||||||
last_task = await get_last_completed_task(db, datasource.id)
|
last_task = await get_last_completed_task(db, datasource.id)
|
||||||
|
endpoint = await config.get_url(datasource.source, db)
|
||||||
data_count_result = await db.execute(
|
data_count_result = await db.execute(
|
||||||
select(func.count(CollectedData.id)).where(CollectedData.source == datasource.source)
|
select(func.count(CollectedData.id)).where(CollectedData.source == datasource.source)
|
||||||
)
|
)
|
||||||
@@ -105,10 +108,12 @@ async def list_datasources(
|
|||||||
"frequency_minutes": datasource.frequency_minutes,
|
"frequency_minutes": datasource.frequency_minutes,
|
||||||
"is_active": datasource.is_active,
|
"is_active": datasource.is_active,
|
||||||
"collector_class": datasource.collector_class,
|
"collector_class": datasource.collector_class,
|
||||||
|
"endpoint": endpoint,
|
||||||
"last_run": last_run,
|
"last_run": last_run,
|
||||||
"is_running": running_task is not None,
|
"is_running": running_task is not None,
|
||||||
"task_id": running_task.id if running_task else None,
|
"task_id": running_task.id if running_task else None,
|
||||||
"progress": running_task.progress if running_task else None,
|
"progress": running_task.progress if running_task else None,
|
||||||
|
"phase": running_task.phase if running_task else None,
|
||||||
"records_processed": running_task.records_processed if running_task else None,
|
"records_processed": running_task.records_processed if running_task else None,
|
||||||
"total_records": running_task.total_records if running_task else None,
|
"total_records": running_task.total_records if running_task else None,
|
||||||
}
|
}
|
||||||
@@ -127,6 +132,9 @@ async def get_datasource(
|
|||||||
if not datasource:
|
if not datasource:
|
||||||
raise HTTPException(status_code=404, detail="Data source not found")
|
raise HTTPException(status_code=404, detail="Data source not found")
|
||||||
|
|
||||||
|
config = get_data_sources_config()
|
||||||
|
endpoint = await config.get_url(datasource.source, db)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"id": datasource.id,
|
"id": datasource.id,
|
||||||
"name": datasource.name,
|
"name": datasource.name,
|
||||||
@@ -136,6 +144,7 @@ async def get_datasource(
|
|||||||
"frequency_minutes": datasource.frequency_minutes,
|
"frequency_minutes": datasource.frequency_minutes,
|
||||||
"collector_class": datasource.collector_class,
|
"collector_class": datasource.collector_class,
|
||||||
"source": datasource.source,
|
"source": datasource.source,
|
||||||
|
"endpoint": endpoint,
|
||||||
"is_active": datasource.is_active,
|
"is_active": datasource.is_active,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,9 +221,16 @@ async def trigger_datasource(
|
|||||||
if not success:
|
if not success:
|
||||||
raise HTTPException(status_code=500, detail=f"Failed to trigger collector '{datasource.source}'")
|
raise HTTPException(status_code=500, detail=f"Failed to trigger collector '{datasource.source}'")
|
||||||
|
|
||||||
|
task_id = None
|
||||||
|
for _ in range(10):
|
||||||
|
task_id = await get_latest_task_id_for_datasource(datasource.id)
|
||||||
|
if task_id is not None:
|
||||||
|
break
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"status": "triggered",
|
"status": "triggered",
|
||||||
"source_id": datasource.id,
|
"source_id": datasource.id,
|
||||||
|
"task_id": task_id,
|
||||||
"collector_name": datasource.source,
|
"collector_name": datasource.source,
|
||||||
"message": f"Collector '{datasource.source}' has been triggered",
|
"message": f"Collector '{datasource.source}' has been triggered",
|
||||||
}
|
}
|
||||||
@@ -252,21 +268,29 @@ async def clear_datasource_data(
|
|||||||
@router.get("/{source_id}/task-status")
|
@router.get("/{source_id}/task-status")
|
||||||
async def get_task_status(
|
async def get_task_status(
|
||||||
source_id: str,
|
source_id: str,
|
||||||
|
task_id: Optional[int] = None,
|
||||||
db: AsyncSession = Depends(get_db),
|
db: AsyncSession = Depends(get_db),
|
||||||
):
|
):
|
||||||
datasource = await get_datasource_record(db, source_id)
|
datasource = await get_datasource_record(db, source_id)
|
||||||
if not datasource:
|
if not datasource:
|
||||||
raise HTTPException(status_code=404, detail="Data source not found")
|
raise HTTPException(status_code=404, detail="Data source not found")
|
||||||
|
|
||||||
running_task = await get_running_task(db, datasource.id)
|
if task_id is not None:
|
||||||
if not running_task:
|
task = await db.get(CollectionTask, task_id)
|
||||||
return {"is_running": False, "task_id": None, "progress": None}
|
if not task or task.datasource_id != datasource.id:
|
||||||
|
raise HTTPException(status_code=404, detail="Task not found")
|
||||||
|
else:
|
||||||
|
task = await get_running_task(db, datasource.id)
|
||||||
|
|
||||||
|
if not task:
|
||||||
|
return {"is_running": False, "task_id": None, "progress": None, "phase": None, "status": "idle"}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"is_running": True,
|
"is_running": task.status == "running",
|
||||||
"task_id": running_task.id,
|
"task_id": task.id,
|
||||||
"progress": running_task.progress,
|
"progress": task.progress,
|
||||||
"records_processed": running_task.records_processed,
|
"phase": task.phase,
|
||||||
"total_records": running_task.total_records,
|
"records_processed": task.records_processed,
|
||||||
"status": running_task.status,
|
"total_records": task.total_records,
|
||||||
|
"status": task.status,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from sqlalchemy import select, func
|
from sqlalchemy import select, func
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
|
from app.core.collected_data_fields import get_record_field
|
||||||
from app.db.session import get_db
|
from app.db.session import get_db
|
||||||
from app.models.collected_data import CollectedData
|
from app.models.collected_data import CollectedData
|
||||||
from app.services.cable_graph import build_graph_from_data, CableGraph
|
from app.services.cable_graph import build_graph_from_data, CableGraph
|
||||||
@@ -83,9 +84,9 @@ def convert_cable_to_geojson(records: List[CollectedData]) -> Dict[str, Any]:
|
|||||||
"rfs": metadata.get("rfs"),
|
"rfs": metadata.get("rfs"),
|
||||||
"RFS": metadata.get("rfs"),
|
"RFS": metadata.get("rfs"),
|
||||||
"status": metadata.get("status", "active"),
|
"status": metadata.get("status", "active"),
|
||||||
"length": record.value,
|
"length": get_record_field(record, "value"),
|
||||||
"length_km": record.value,
|
"length_km": get_record_field(record, "value"),
|
||||||
"SHAPE__Length": record.value,
|
"SHAPE__Length": get_record_field(record, "value"),
|
||||||
"url": metadata.get("url"),
|
"url": metadata.get("url"),
|
||||||
"color": metadata.get("color"),
|
"color": metadata.get("color"),
|
||||||
"year": metadata.get("year"),
|
"year": metadata.get("year"),
|
||||||
@@ -101,8 +102,10 @@ def convert_landing_point_to_geojson(records: List[CollectedData], city_to_cable
|
|||||||
|
|
||||||
for record in records:
|
for record in records:
|
||||||
try:
|
try:
|
||||||
lat = float(record.latitude) if record.latitude else None
|
latitude = get_record_field(record, "latitude")
|
||||||
lon = float(record.longitude) if record.longitude else None
|
longitude = get_record_field(record, "longitude")
|
||||||
|
lat = float(latitude) if latitude else None
|
||||||
|
lon = float(longitude) if longitude else None
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -116,8 +119,8 @@ def convert_landing_point_to_geojson(records: List[CollectedData], city_to_cable
|
|||||||
"id": record.id,
|
"id": record.id,
|
||||||
"source_id": record.source_id,
|
"source_id": record.source_id,
|
||||||
"name": record.name,
|
"name": record.name,
|
||||||
"country": record.country,
|
"country": get_record_field(record, "country"),
|
||||||
"city": record.city,
|
"city": get_record_field(record, "city"),
|
||||||
"is_tbd": metadata.get("is_tbd", False),
|
"is_tbd": metadata.get("is_tbd", False),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -185,9 +188,11 @@ def convert_supercomputer_to_geojson(records: List[CollectedData]) -> Dict[str,
|
|||||||
|
|
||||||
for record in records:
|
for record in records:
|
||||||
try:
|
try:
|
||||||
lat = float(record.latitude) if record.latitude and record.latitude != "0.0" else None
|
latitude = get_record_field(record, "latitude")
|
||||||
|
longitude = get_record_field(record, "longitude")
|
||||||
|
lat = float(latitude) if latitude and latitude != "0.0" else None
|
||||||
lon = (
|
lon = (
|
||||||
float(record.longitude) if record.longitude and record.longitude != "0.0" else None
|
float(longitude) if longitude and longitude != "0.0" else None
|
||||||
)
|
)
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
lat, lon = None, None
|
lat, lon = None, None
|
||||||
@@ -203,12 +208,12 @@ def convert_supercomputer_to_geojson(records: List[CollectedData]) -> Dict[str,
|
|||||||
"id": record.id,
|
"id": record.id,
|
||||||
"name": record.name,
|
"name": record.name,
|
||||||
"rank": metadata.get("rank"),
|
"rank": metadata.get("rank"),
|
||||||
"r_max": record.value,
|
"r_max": get_record_field(record, "rmax"),
|
||||||
"r_peak": metadata.get("r_peak"),
|
"r_peak": get_record_field(record, "rpeak"),
|
||||||
"cores": metadata.get("cores"),
|
"cores": get_record_field(record, "cores"),
|
||||||
"power": metadata.get("power"),
|
"power": get_record_field(record, "power"),
|
||||||
"country": record.country,
|
"country": get_record_field(record, "country"),
|
||||||
"city": record.city,
|
"city": get_record_field(record, "city"),
|
||||||
"data_type": "supercomputer",
|
"data_type": "supercomputer",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -223,8 +228,10 @@ def convert_gpu_cluster_to_geojson(records: List[CollectedData]) -> Dict[str, An
|
|||||||
|
|
||||||
for record in records:
|
for record in records:
|
||||||
try:
|
try:
|
||||||
lat = float(record.latitude) if record.latitude else None
|
latitude = get_record_field(record, "latitude")
|
||||||
lon = float(record.longitude) if record.longitude else None
|
longitude = get_record_field(record, "longitude")
|
||||||
|
lat = float(latitude) if latitude else None
|
||||||
|
lon = float(longitude) if longitude else None
|
||||||
except (ValueError, TypeError):
|
except (ValueError, TypeError):
|
||||||
lat, lon = None, None
|
lat, lon = None, None
|
||||||
|
|
||||||
@@ -238,8 +245,8 @@ def convert_gpu_cluster_to_geojson(records: List[CollectedData]) -> Dict[str, An
|
|||||||
"properties": {
|
"properties": {
|
||||||
"id": record.id,
|
"id": record.id,
|
||||||
"name": record.name,
|
"name": record.name,
|
||||||
"country": record.country,
|
"country": get_record_field(record, "country"),
|
||||||
"city": record.city,
|
"city": get_record_field(record, "city"),
|
||||||
"metadata": metadata,
|
"metadata": metadata,
|
||||||
"data_type": "gpu_cluster",
|
"data_type": "gpu_cluster",
|
||||||
},
|
},
|
||||||
|
|||||||
62
backend/app/core/collected_data_fields.py
Normal file
62
backend/app/core/collected_data_fields.py
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
|
||||||
|
FIELD_ALIASES = {
|
||||||
|
"country": ("country",),
|
||||||
|
"city": ("city",),
|
||||||
|
"latitude": ("latitude",),
|
||||||
|
"longitude": ("longitude",),
|
||||||
|
"value": ("value",),
|
||||||
|
"unit": ("unit",),
|
||||||
|
"cores": ("cores",),
|
||||||
|
"rmax": ("rmax", "r_max"),
|
||||||
|
"rpeak": ("rpeak", "r_peak"),
|
||||||
|
"power": ("power",),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_metadata_field(metadata: Optional[Dict[str, Any]], field: str, fallback: Any = None) -> Any:
|
||||||
|
if isinstance(metadata, dict):
|
||||||
|
for key in FIELD_ALIASES.get(field, (field,)):
|
||||||
|
value = metadata.get(key)
|
||||||
|
if value not in (None, ""):
|
||||||
|
return value
|
||||||
|
return fallback
|
||||||
|
|
||||||
|
|
||||||
|
def build_dynamic_metadata(
|
||||||
|
metadata: Optional[Dict[str, Any]],
|
||||||
|
*,
|
||||||
|
country: Any = None,
|
||||||
|
city: Any = None,
|
||||||
|
latitude: Any = None,
|
||||||
|
longitude: Any = None,
|
||||||
|
value: Any = None,
|
||||||
|
unit: Any = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
merged = dict(metadata) if isinstance(metadata, dict) else {}
|
||||||
|
|
||||||
|
fallbacks = {
|
||||||
|
"country": country,
|
||||||
|
"city": city,
|
||||||
|
"latitude": latitude,
|
||||||
|
"longitude": longitude,
|
||||||
|
"value": value,
|
||||||
|
"unit": unit,
|
||||||
|
}
|
||||||
|
|
||||||
|
for field, fallback in fallbacks.items():
|
||||||
|
if fallback not in (None, "") and get_metadata_field(merged, field) in (None, ""):
|
||||||
|
merged[field] = fallback
|
||||||
|
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
def get_record_field(record: Any, field: str) -> Any:
|
||||||
|
metadata = getattr(record, "extra_data", None) or {}
|
||||||
|
fallback_attr = field
|
||||||
|
if field in {"cores", "rmax", "rpeak", "power"}:
|
||||||
|
fallback = None
|
||||||
|
else:
|
||||||
|
fallback = getattr(record, fallback_attr, None)
|
||||||
|
return get_metadata_field(metadata, field, fallback=fallback)
|
||||||
280
backend/app/core/countries.py
Normal file
280
backend/app/core/countries.py
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
import re
|
||||||
|
from typing import Any, Optional
|
||||||
|
|
||||||
|
|
||||||
|
COUNTRY_ENTRIES = [
|
||||||
|
("阿富汗", ["Afghanistan", "AF", "AFG"]),
|
||||||
|
("阿尔巴尼亚", ["Albania", "AL", "ALB"]),
|
||||||
|
("阿尔及利亚", ["Algeria", "DZ", "DZA"]),
|
||||||
|
("安道尔", ["Andorra", "AD", "AND"]),
|
||||||
|
("安哥拉", ["Angola", "AO", "AGO"]),
|
||||||
|
("安提瓜和巴布达", ["Antigua and Barbuda", "AG", "ATG"]),
|
||||||
|
("阿根廷", ["Argentina", "AR", "ARG"]),
|
||||||
|
("亚美尼亚", ["Armenia", "AM", "ARM"]),
|
||||||
|
("澳大利亚", ["Australia", "AU", "AUS"]),
|
||||||
|
("奥地利", ["Austria", "AT", "AUT"]),
|
||||||
|
("阿塞拜疆", ["Azerbaijan", "AZ", "AZE"]),
|
||||||
|
("巴哈马", ["Bahamas", "BS", "BHS"]),
|
||||||
|
("巴林", ["Bahrain", "BH", "BHR"]),
|
||||||
|
("孟加拉国", ["Bangladesh", "BD", "BGD"]),
|
||||||
|
("巴巴多斯", ["Barbados", "BB", "BRB"]),
|
||||||
|
("白俄罗斯", ["Belarus", "BY", "BLR"]),
|
||||||
|
("比利时", ["Belgium", "BE", "BEL"]),
|
||||||
|
("伯利兹", ["Belize", "BZ", "BLZ"]),
|
||||||
|
("贝宁", ["Benin", "BJ", "BEN"]),
|
||||||
|
("不丹", ["Bhutan", "BT", "BTN"]),
|
||||||
|
("玻利维亚", ["Bolivia", "BO", "BOL", "Bolivia (Plurinational State of)"]),
|
||||||
|
("波斯尼亚和黑塞哥维那", ["Bosnia and Herzegovina", "BA", "BIH"]),
|
||||||
|
("博茨瓦纳", ["Botswana", "BW", "BWA"]),
|
||||||
|
("巴西", ["Brazil", "BR", "BRA"]),
|
||||||
|
("文莱", ["Brunei", "BN", "BRN", "Brunei Darussalam"]),
|
||||||
|
("保加利亚", ["Bulgaria", "BG", "BGR"]),
|
||||||
|
("布基纳法索", ["Burkina Faso", "BF", "BFA"]),
|
||||||
|
("布隆迪", ["Burundi", "BI", "BDI"]),
|
||||||
|
("柬埔寨", ["Cambodia", "KH", "KHM"]),
|
||||||
|
("喀麦隆", ["Cameroon", "CM", "CMR"]),
|
||||||
|
("加拿大", ["Canada", "CA", "CAN"]),
|
||||||
|
("佛得角", ["Cape Verde", "CV", "CPV", "Cabo Verde"]),
|
||||||
|
("中非", ["Central African Republic", "CF", "CAF"]),
|
||||||
|
("乍得", ["Chad", "TD", "TCD"]),
|
||||||
|
("智利", ["Chile", "CL", "CHL"]),
|
||||||
|
("中国", ["China", "CN", "CHN", "Mainland China", "PRC", "People's Republic of China"]),
|
||||||
|
("中国(香港)", ["Hong Kong", "HK", "HKG", "Hong Kong SAR", "China Hong Kong", "Hong Kong, China"]),
|
||||||
|
("中国(澳门)", ["Macao", "Macau", "MO", "MAC", "Macao SAR", "China Macao", "Macau, China"]),
|
||||||
|
("中国(台湾)", ["Taiwan", "TW", "TWN", "Chinese Taipei", "Taiwan, China"]),
|
||||||
|
("哥伦比亚", ["Colombia", "CO", "COL"]),
|
||||||
|
("科摩罗", ["Comoros", "KM", "COM"]),
|
||||||
|
("刚果(布)", ["Republic of the Congo", "Congo", "Congo-Brazzaville", "CG", "COG"]),
|
||||||
|
("刚果(金)", ["Democratic Republic of the Congo", "DR Congo", "Congo-Kinshasa", "CD", "COD"]),
|
||||||
|
("哥斯达黎加", ["Costa Rica", "CR", "CRI"]),
|
||||||
|
("科特迪瓦", ["Cote d'Ivoire", "Côte d'Ivoire", "Ivory Coast", "CI", "CIV"]),
|
||||||
|
("克罗地亚", ["Croatia", "HR", "HRV"]),
|
||||||
|
("古巴", ["Cuba", "CU", "CUB"]),
|
||||||
|
("塞浦路斯", ["Cyprus", "CY", "CYP"]),
|
||||||
|
("捷克", ["Czech Republic", "Czechia", "CZ", "CZE"]),
|
||||||
|
("丹麦", ["Denmark", "DK", "DNK"]),
|
||||||
|
("吉布提", ["Djibouti", "DJ", "DJI"]),
|
||||||
|
("多米尼克", ["Dominica", "DM", "DMA"]),
|
||||||
|
("多米尼加", ["Dominican Republic", "DO", "DOM"]),
|
||||||
|
("厄瓜多尔", ["Ecuador", "EC", "ECU"]),
|
||||||
|
("埃及", ["Egypt", "EG", "EGY"]),
|
||||||
|
("萨尔瓦多", ["El Salvador", "SV", "SLV"]),
|
||||||
|
("赤道几内亚", ["Equatorial Guinea", "GQ", "GNQ"]),
|
||||||
|
("厄立特里亚", ["Eritrea", "ER", "ERI"]),
|
||||||
|
("爱沙尼亚", ["Estonia", "EE", "EST"]),
|
||||||
|
("埃斯瓦蒂尼", ["Eswatini", "SZ", "SWZ", "Swaziland"]),
|
||||||
|
("埃塞俄比亚", ["Ethiopia", "ET", "ETH"]),
|
||||||
|
("斐济", ["Fiji", "FJ", "FJI"]),
|
||||||
|
("芬兰", ["Finland", "FI", "FIN"]),
|
||||||
|
("法国", ["France", "FR", "FRA"]),
|
||||||
|
("加蓬", ["Gabon", "GA", "GAB"]),
|
||||||
|
("冈比亚", ["Gambia", "GM", "GMB"]),
|
||||||
|
("格鲁吉亚", ["Georgia", "GE", "GEO"]),
|
||||||
|
("德国", ["Germany", "DE", "DEU"]),
|
||||||
|
("加纳", ["Ghana", "GH", "GHA"]),
|
||||||
|
("希腊", ["Greece", "GR", "GRC"]),
|
||||||
|
("格林纳达", ["Grenada", "GD", "GRD"]),
|
||||||
|
("危地马拉", ["Guatemala", "GT", "GTM"]),
|
||||||
|
("几内亚", ["Guinea", "GN", "GIN"]),
|
||||||
|
("几内亚比绍", ["Guinea-Bissau", "GW", "GNB"]),
|
||||||
|
("圭亚那", ["Guyana", "GY", "GUY"]),
|
||||||
|
("海地", ["Haiti", "HT", "HTI"]),
|
||||||
|
("洪都拉斯", ["Honduras", "HN", "HND"]),
|
||||||
|
("匈牙利", ["Hungary", "HU", "HUN"]),
|
||||||
|
("冰岛", ["Iceland", "IS", "ISL"]),
|
||||||
|
("印度", ["India", "IN", "IND"]),
|
||||||
|
("印度尼西亚", ["Indonesia", "ID", "IDN"]),
|
||||||
|
("伊朗", ["Iran", "IR", "IRN", "Iran (Islamic Republic of)"]),
|
||||||
|
("伊拉克", ["Iraq", "IQ", "IRQ"]),
|
||||||
|
("爱尔兰", ["Ireland", "IE", "IRL"]),
|
||||||
|
("以色列", ["Israel", "IL", "ISR"]),
|
||||||
|
("意大利", ["Italy", "IT", "ITA"]),
|
||||||
|
("牙买加", ["Jamaica", "JM", "JAM"]),
|
||||||
|
("日本", ["Japan", "JP", "JPN"]),
|
||||||
|
("约旦", ["Jordan", "JO", "JOR"]),
|
||||||
|
("哈萨克斯坦", ["Kazakhstan", "KZ", "KAZ"]),
|
||||||
|
("肯尼亚", ["Kenya", "KE", "KEN"]),
|
||||||
|
("基里巴斯", ["Kiribati", "KI", "KIR"]),
|
||||||
|
("朝鲜", ["North Korea", "Korea, DPRK", "Democratic People's Republic of Korea", "KP", "PRK"]),
|
||||||
|
("韩国", ["South Korea", "Republic of Korea", "Korea", "KR", "KOR"]),
|
||||||
|
("科威特", ["Kuwait", "KW", "KWT"]),
|
||||||
|
("吉尔吉斯斯坦", ["Kyrgyzstan", "KG", "KGZ"]),
|
||||||
|
("老挝", ["Laos", "Lao PDR", "Lao People's Democratic Republic", "LA", "LAO"]),
|
||||||
|
("拉脱维亚", ["Latvia", "LV", "LVA"]),
|
||||||
|
("黎巴嫩", ["Lebanon", "LB", "LBN"]),
|
||||||
|
("莱索托", ["Lesotho", "LS", "LSO"]),
|
||||||
|
("利比里亚", ["Liberia", "LR", "LBR"]),
|
||||||
|
("利比亚", ["Libya", "LY", "LBY"]),
|
||||||
|
("列支敦士登", ["Liechtenstein", "LI", "LIE"]),
|
||||||
|
("立陶宛", ["Lithuania", "LT", "LTU"]),
|
||||||
|
("卢森堡", ["Luxembourg", "LU", "LUX"]),
|
||||||
|
("马达加斯加", ["Madagascar", "MG", "MDG"]),
|
||||||
|
("马拉维", ["Malawi", "MW", "MWI"]),
|
||||||
|
("马来西亚", ["Malaysia", "MY", "MYS"]),
|
||||||
|
("马尔代夫", ["Maldives", "MV", "MDV"]),
|
||||||
|
("马里", ["Mali", "ML", "MLI"]),
|
||||||
|
("马耳他", ["Malta", "MT", "MLT"]),
|
||||||
|
("马绍尔群岛", ["Marshall Islands", "MH", "MHL"]),
|
||||||
|
("毛里塔尼亚", ["Mauritania", "MR", "MRT"]),
|
||||||
|
("毛里求斯", ["Mauritius", "MU", "MUS"]),
|
||||||
|
("墨西哥", ["Mexico", "MX", "MEX"]),
|
||||||
|
("密克罗尼西亚", ["Micronesia", "FM", "FSM", "Federated States of Micronesia"]),
|
||||||
|
("摩尔多瓦", ["Moldova", "MD", "MDA", "Republic of Moldova"]),
|
||||||
|
("摩纳哥", ["Monaco", "MC", "MCO"]),
|
||||||
|
("蒙古", ["Mongolia", "MN", "MNG"]),
|
||||||
|
("黑山", ["Montenegro", "ME", "MNE"]),
|
||||||
|
("摩洛哥", ["Morocco", "MA", "MAR"]),
|
||||||
|
("莫桑比克", ["Mozambique", "MZ", "MOZ"]),
|
||||||
|
("缅甸", ["Myanmar", "MM", "MMR", "Burma"]),
|
||||||
|
("纳米比亚", ["Namibia", "NA", "NAM"]),
|
||||||
|
("瑙鲁", ["Nauru", "NR", "NRU"]),
|
||||||
|
("尼泊尔", ["Nepal", "NP", "NPL"]),
|
||||||
|
("荷兰", ["Netherlands", "NL", "NLD"]),
|
||||||
|
("新西兰", ["New Zealand", "NZ", "NZL"]),
|
||||||
|
("尼加拉瓜", ["Nicaragua", "NI", "NIC"]),
|
||||||
|
("尼日尔", ["Niger", "NE", "NER"]),
|
||||||
|
("尼日利亚", ["Nigeria", "NG", "NGA"]),
|
||||||
|
("北马其顿", ["North Macedonia", "MK", "MKD", "Macedonia"]),
|
||||||
|
("挪威", ["Norway", "NO", "NOR"]),
|
||||||
|
("阿曼", ["Oman", "OM", "OMN"]),
|
||||||
|
("巴基斯坦", ["Pakistan", "PK", "PAK"]),
|
||||||
|
("帕劳", ["Palau", "PW", "PLW"]),
|
||||||
|
("巴勒斯坦", ["Palestine", "PS", "PSE", "State of Palestine"]),
|
||||||
|
("巴拿马", ["Panama", "PA", "PAN"]),
|
||||||
|
("巴布亚新几内亚", ["Papua New Guinea", "PG", "PNG"]),
|
||||||
|
("巴拉圭", ["Paraguay", "PY", "PRY"]),
|
||||||
|
("秘鲁", ["Peru", "PE", "PER"]),
|
||||||
|
("菲律宾", ["Philippines", "PH", "PHL"]),
|
||||||
|
("波兰", ["Poland", "PL", "POL"]),
|
||||||
|
("葡萄牙", ["Portugal", "PT", "PRT"]),
|
||||||
|
("卡塔尔", ["Qatar", "QA", "QAT"]),
|
||||||
|
("罗马尼亚", ["Romania", "RO", "ROU"]),
|
||||||
|
("俄罗斯", ["Russia", "Russian Federation", "RU", "RUS"]),
|
||||||
|
("卢旺达", ["Rwanda", "RW", "RWA"]),
|
||||||
|
("圣基茨和尼维斯", ["Saint Kitts and Nevis", "KN", "KNA"]),
|
||||||
|
("圣卢西亚", ["Saint Lucia", "LC", "LCA"]),
|
||||||
|
("圣文森特和格林纳丁斯", ["Saint Vincent and the Grenadines", "VC", "VCT"]),
|
||||||
|
("萨摩亚", ["Samoa", "WS", "WSM"]),
|
||||||
|
("圣马力诺", ["San Marino", "SM", "SMR"]),
|
||||||
|
("圣多美和普林西比", ["Sao Tome and Principe", "ST", "STP", "São Tomé and Príncipe"]),
|
||||||
|
("沙特阿拉伯", ["Saudi Arabia", "SA", "SAU"]),
|
||||||
|
("塞内加尔", ["Senegal", "SN", "SEN"]),
|
||||||
|
("塞尔维亚", ["Serbia", "RS", "SRB", "Kosovo", "XK", "XKS", "Republic of Kosovo"]),
|
||||||
|
("塞舌尔", ["Seychelles", "SC", "SYC"]),
|
||||||
|
("塞拉利昂", ["Sierra Leone", "SL", "SLE"]),
|
||||||
|
("新加坡", ["Singapore", "SG", "SGP"]),
|
||||||
|
("斯洛伐克", ["Slovakia", "SK", "SVK"]),
|
||||||
|
("斯洛文尼亚", ["Slovenia", "SI", "SVN"]),
|
||||||
|
("所罗门群岛", ["Solomon Islands", "SB", "SLB"]),
|
||||||
|
("索马里", ["Somalia", "SO", "SOM"]),
|
||||||
|
("南非", ["South Africa", "ZA", "ZAF"]),
|
||||||
|
("南苏丹", ["South Sudan", "SS", "SSD"]),
|
||||||
|
("西班牙", ["Spain", "ES", "ESP"]),
|
||||||
|
("斯里兰卡", ["Sri Lanka", "LK", "LKA"]),
|
||||||
|
("苏丹", ["Sudan", "SD", "SDN"]),
|
||||||
|
("苏里南", ["Suriname", "SR", "SUR"]),
|
||||||
|
("瑞典", ["Sweden", "SE", "SWE"]),
|
||||||
|
("瑞士", ["Switzerland", "CH", "CHE"]),
|
||||||
|
("叙利亚", ["Syria", "SY", "SYR", "Syrian Arab Republic"]),
|
||||||
|
("塔吉克斯坦", ["Tajikistan", "TJ", "TJK"]),
|
||||||
|
("坦桑尼亚", ["Tanzania", "TZ", "TZA", "United Republic of Tanzania"]),
|
||||||
|
("泰国", ["Thailand", "TH", "THA"]),
|
||||||
|
("东帝汶", ["Timor-Leste", "East Timor", "TL", "TLS"]),
|
||||||
|
("多哥", ["Togo", "TG", "TGO"]),
|
||||||
|
("汤加", ["Tonga", "TO", "TON"]),
|
||||||
|
("特立尼达和多巴哥", ["Trinidad and Tobago", "TT", "TTO"]),
|
||||||
|
("突尼斯", ["Tunisia", "TN", "TUN"]),
|
||||||
|
("土耳其", ["Turkey", "TR", "TUR", "Türkiye"]),
|
||||||
|
("土库曼斯坦", ["Turkmenistan", "TM", "TKM"]),
|
||||||
|
("图瓦卢", ["Tuvalu", "TV", "TUV"]),
|
||||||
|
("乌干达", ["Uganda", "UG", "UGA"]),
|
||||||
|
("乌克兰", ["Ukraine", "UA", "UKR"]),
|
||||||
|
("阿联酋", ["United Arab Emirates", "AE", "ARE", "UAE"]),
|
||||||
|
("英国", ["United Kingdom", "UK", "GB", "GBR", "Great Britain", "Britain", "England"]),
|
||||||
|
("美国", ["United States", "United States of America", "US", "USA", "U.S.", "U.S.A."]),
|
||||||
|
("乌拉圭", ["Uruguay", "UY", "URY"]),
|
||||||
|
("乌兹别克斯坦", ["Uzbekistan", "UZ", "UZB"]),
|
||||||
|
("瓦努阿图", ["Vanuatu", "VU", "VUT"]),
|
||||||
|
("梵蒂冈", ["Vatican City", "Holy See", "VA", "VAT"]),
|
||||||
|
("委内瑞拉", ["Venezuela", "VE", "VEN", "Venezuela (Bolivarian Republic of)"]),
|
||||||
|
("越南", ["Vietnam", "Viet Nam", "VN", "VNM"]),
|
||||||
|
("也门", ["Yemen", "YE", "YEM"]),
|
||||||
|
("赞比亚", ["Zambia", "ZM", "ZMB"]),
|
||||||
|
("津巴布韦", ["Zimbabwe", "ZW", "ZWE"]),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
COUNTRY_OPTIONS = [entry[0] for entry in COUNTRY_ENTRIES]
|
||||||
|
CANONICAL_COUNTRY_SET = set(COUNTRY_OPTIONS)
|
||||||
|
INVALID_COUNTRY_VALUES = {
|
||||||
|
"",
|
||||||
|
"-",
|
||||||
|
"--",
|
||||||
|
"unknown",
|
||||||
|
"n/a",
|
||||||
|
"na",
|
||||||
|
"none",
|
||||||
|
"null",
|
||||||
|
"global",
|
||||||
|
"world",
|
||||||
|
"worldwide",
|
||||||
|
"xx",
|
||||||
|
}
|
||||||
|
NUMERIC_LIKE_PATTERN = re.compile(r"^[\d\s,._%+\-]+$")
|
||||||
|
|
||||||
|
COUNTRY_ALIAS_MAP = {}
|
||||||
|
COUNTRY_VARIANTS_MAP = {}
|
||||||
|
for canonical, aliases in COUNTRY_ENTRIES:
|
||||||
|
COUNTRY_ALIAS_MAP[canonical.casefold()] = canonical
|
||||||
|
variants = [canonical, *aliases]
|
||||||
|
COUNTRY_VARIANTS_MAP[canonical] = variants
|
||||||
|
for alias in aliases:
|
||||||
|
COUNTRY_ALIAS_MAP[alias.casefold()] = canonical
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_country(value: Any) -> Optional[str]:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if not isinstance(value, str):
|
||||||
|
return None
|
||||||
|
|
||||||
|
normalized = re.sub(r"\s+", " ", value.strip())
|
||||||
|
normalized = normalized.replace("(", "(").replace(")", ")")
|
||||||
|
|
||||||
|
if not normalized:
|
||||||
|
return None
|
||||||
|
|
||||||
|
lowered = normalized.casefold()
|
||||||
|
if lowered in INVALID_COUNTRY_VALUES:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if NUMERIC_LIKE_PATTERN.fullmatch(normalized):
|
||||||
|
return None
|
||||||
|
|
||||||
|
if normalized in CANONICAL_COUNTRY_SET:
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
return COUNTRY_ALIAS_MAP.get(lowered)
|
||||||
|
|
||||||
|
|
||||||
|
def get_country_search_variants(value: Any) -> list[str]:
|
||||||
|
canonical = normalize_country(value)
|
||||||
|
if canonical is None:
|
||||||
|
return []
|
||||||
|
|
||||||
|
variants = []
|
||||||
|
seen = set()
|
||||||
|
for item in COUNTRY_VARIANTS_MAP.get(canonical, [canonical]):
|
||||||
|
if not isinstance(item, str):
|
||||||
|
continue
|
||||||
|
normalized = re.sub(r"\s+", " ", item.strip())
|
||||||
|
if not normalized:
|
||||||
|
continue
|
||||||
|
key = normalized.casefold()
|
||||||
|
if key in seen:
|
||||||
|
continue
|
||||||
|
seen.add(key)
|
||||||
|
variants.append(normalized)
|
||||||
|
|
||||||
|
return variants
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
from typing import AsyncGenerator
|
from typing import AsyncGenerator
|
||||||
|
|
||||||
|
from sqlalchemy import text
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||||
from sqlalchemy.orm import declarative_base
|
from sqlalchemy.orm import declarative_base
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ async def init_db():
|
|||||||
import app.models.user # noqa: F401
|
import app.models.user # noqa: F401
|
||||||
import app.models.gpu_cluster # noqa: F401
|
import app.models.gpu_cluster # noqa: F401
|
||||||
import app.models.task # noqa: F401
|
import app.models.task # noqa: F401
|
||||||
|
import app.models.data_snapshot # noqa: F401
|
||||||
import app.models.datasource # noqa: F401
|
import app.models.datasource # noqa: F401
|
||||||
import app.models.datasource_config # noqa: F401
|
import app.models.datasource_config # noqa: F401
|
||||||
import app.models.alert # noqa: F401
|
import app.models.alert # noqa: F401
|
||||||
@@ -71,6 +73,55 @@ async def init_db():
|
|||||||
|
|
||||||
async with engine.begin() as conn:
|
async with engine.begin() as conn:
|
||||||
await conn.run_sync(Base.metadata.create_all)
|
await conn.run_sync(Base.metadata.create_all)
|
||||||
|
await conn.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
ALTER TABLE collected_data
|
||||||
|
ADD COLUMN IF NOT EXISTS snapshot_id INTEGER,
|
||||||
|
ADD COLUMN IF NOT EXISTS task_id INTEGER,
|
||||||
|
ADD COLUMN IF NOT EXISTS entity_key VARCHAR(255),
|
||||||
|
ADD COLUMN IF NOT EXISTS is_current BOOLEAN DEFAULT TRUE,
|
||||||
|
ADD COLUMN IF NOT EXISTS previous_record_id INTEGER,
|
||||||
|
ADD COLUMN IF NOT EXISTS change_type VARCHAR(20),
|
||||||
|
ADD COLUMN IF NOT EXISTS change_summary JSONB DEFAULT '{}'::jsonb,
|
||||||
|
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await conn.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
ALTER TABLE collection_tasks
|
||||||
|
ADD COLUMN IF NOT EXISTS phase VARCHAR(30) DEFAULT 'queued'
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await conn.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_collected_data_source_source_id
|
||||||
|
ON collected_data (source, source_id)
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await conn.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
UPDATE collected_data
|
||||||
|
SET entity_key = source || ':' || COALESCE(source_id, id::text)
|
||||||
|
WHERE entity_key IS NULL
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
await conn.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
UPDATE collected_data
|
||||||
|
SET is_current = TRUE
|
||||||
|
WHERE is_current IS NULL
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
async with async_session_factory() as session:
|
async with async_session_factory() as session:
|
||||||
await seed_default_datasources(session)
|
await seed_default_datasources(session)
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ from app.api.v1 import websocket
|
|||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.websocket.broadcaster import broadcaster
|
from app.core.websocket.broadcaster import broadcaster
|
||||||
from app.db.session import init_db
|
from app.db.session import init_db
|
||||||
from app.services.scheduler import start_scheduler, stop_scheduler, sync_scheduler_with_datasources
|
from app.services.scheduler import (
|
||||||
|
cleanup_stale_running_tasks,
|
||||||
|
start_scheduler,
|
||||||
|
stop_scheduler,
|
||||||
|
sync_scheduler_with_datasources,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class WebSocketCORSMiddleware(BaseHTTPMiddleware):
|
class WebSocketCORSMiddleware(BaseHTTPMiddleware):
|
||||||
@@ -26,6 +31,7 @@ class WebSocketCORSMiddleware(BaseHTTPMiddleware):
|
|||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
await init_db()
|
await init_db()
|
||||||
|
await cleanup_stale_running_tasks()
|
||||||
start_scheduler()
|
start_scheduler()
|
||||||
await sync_scheduler_with_datasources()
|
await sync_scheduler_with_datasources()
|
||||||
broadcaster.start()
|
broadcaster.start()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
from app.models.user import User
|
from app.models.user import User
|
||||||
from app.models.gpu_cluster import GPUCluster
|
from app.models.gpu_cluster import GPUCluster
|
||||||
from app.models.task import CollectionTask
|
from app.models.task import CollectionTask
|
||||||
|
from app.models.data_snapshot import DataSnapshot
|
||||||
from app.models.datasource import DataSource
|
from app.models.datasource import DataSource
|
||||||
from app.models.datasource_config import DataSourceConfig
|
from app.models.datasource_config import DataSourceConfig
|
||||||
from app.models.alert import Alert, AlertSeverity, AlertStatus
|
from app.models.alert import Alert, AlertSeverity, AlertStatus
|
||||||
@@ -10,6 +11,7 @@ __all__ = [
|
|||||||
"User",
|
"User",
|
||||||
"GPUCluster",
|
"GPUCluster",
|
||||||
"CollectionTask",
|
"CollectionTask",
|
||||||
|
"DataSnapshot",
|
||||||
"DataSource",
|
"DataSource",
|
||||||
"DataSourceConfig",
|
"DataSourceConfig",
|
||||||
"SystemSetting",
|
"SystemSetting",
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"""Collected Data model for storing data from all collectors"""
|
"""Collected Data model for storing data from all collectors"""
|
||||||
|
|
||||||
from sqlalchemy import Column, DateTime, Integer, String, Text, JSON, Index
|
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String, Text, JSON, Index
|
||||||
from sqlalchemy.sql import func
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
|
from app.core.collected_data_fields import get_record_field
|
||||||
from app.db.session import Base
|
from app.db.session import Base
|
||||||
|
|
||||||
|
|
||||||
@@ -12,8 +13,11 @@ class CollectedData(Base):
|
|||||||
__tablename__ = "collected_data"
|
__tablename__ = "collected_data"
|
||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
snapshot_id = Column(Integer, ForeignKey("data_snapshots.id"), nullable=True, index=True)
|
||||||
|
task_id = Column(Integer, ForeignKey("collection_tasks.id"), nullable=True, index=True)
|
||||||
source = Column(String(100), nullable=False, index=True) # e.g., "top500", "huggingface_models"
|
source = Column(String(100), nullable=False, index=True) # e.g., "top500", "huggingface_models"
|
||||||
source_id = Column(String(100), index=True) # Original ID from source, e.g., "rank_1"
|
source_id = Column(String(100), index=True) # Original ID from source, e.g., "rank_1"
|
||||||
|
entity_key = Column(String(255), index=True)
|
||||||
data_type = Column(
|
data_type = Column(
|
||||||
String(50), nullable=False, index=True
|
String(50), nullable=False, index=True
|
||||||
) # e.g., "supercomputer", "model", "dataset"
|
) # e.g., "supercomputer", "model", "dataset"
|
||||||
@@ -23,16 +27,6 @@ class CollectedData(Base):
|
|||||||
title = Column(String(500))
|
title = Column(String(500))
|
||||||
description = Column(Text)
|
description = Column(Text)
|
||||||
|
|
||||||
# Location data (for geo visualization)
|
|
||||||
country = Column(String(100))
|
|
||||||
city = Column(String(100))
|
|
||||||
latitude = Column(String(50))
|
|
||||||
longitude = Column(String(50))
|
|
||||||
|
|
||||||
# Performance metrics
|
|
||||||
value = Column(String(100)) # Generic value field (Rmax, Rpeak, etc.)
|
|
||||||
unit = Column(String(20))
|
|
||||||
|
|
||||||
# Additional metadata as JSON
|
# Additional metadata as JSON
|
||||||
extra_data = Column(
|
extra_data = Column(
|
||||||
"metadata", JSON, default={}
|
"metadata", JSON, default={}
|
||||||
@@ -44,11 +38,17 @@ class CollectedData(Base):
|
|||||||
|
|
||||||
# Status
|
# Status
|
||||||
is_valid = Column(Integer, default=1) # 1=valid, 0=invalid
|
is_valid = Column(Integer, default=1) # 1=valid, 0=invalid
|
||||||
|
is_current = Column(Boolean, default=True, index=True)
|
||||||
|
previous_record_id = Column(Integer, ForeignKey("collected_data.id"), nullable=True, index=True)
|
||||||
|
change_type = Column(String(20), nullable=True)
|
||||||
|
change_summary = Column(JSON, default={})
|
||||||
|
deleted_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
|
||||||
# Indexes for common queries
|
# Indexes for common queries
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
Index("idx_collected_data_source_collected", "source", "collected_at"),
|
Index("idx_collected_data_source_collected", "source", "collected_at"),
|
||||||
Index("idx_collected_data_source_type", "source", "data_type"),
|
Index("idx_collected_data_source_type", "source", "data_type"),
|
||||||
|
Index("idx_collected_data_source_source_id", "source", "source_id"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
@@ -58,18 +58,21 @@ class CollectedData(Base):
|
|||||||
"""Convert to dictionary"""
|
"""Convert to dictionary"""
|
||||||
return {
|
return {
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
|
"snapshot_id": self.snapshot_id,
|
||||||
|
"task_id": self.task_id,
|
||||||
"source": self.source,
|
"source": self.source,
|
||||||
"source_id": self.source_id,
|
"source_id": self.source_id,
|
||||||
|
"entity_key": self.entity_key,
|
||||||
"data_type": self.data_type,
|
"data_type": self.data_type,
|
||||||
"name": self.name,
|
"name": self.name,
|
||||||
"title": self.title,
|
"title": self.title,
|
||||||
"description": self.description,
|
"description": self.description,
|
||||||
"country": self.country,
|
"country": get_record_field(self, "country"),
|
||||||
"city": self.city,
|
"city": get_record_field(self, "city"),
|
||||||
"latitude": self.latitude,
|
"latitude": get_record_field(self, "latitude"),
|
||||||
"longitude": self.longitude,
|
"longitude": get_record_field(self, "longitude"),
|
||||||
"value": self.value,
|
"value": get_record_field(self, "value"),
|
||||||
"unit": self.unit,
|
"unit": get_record_field(self, "unit"),
|
||||||
"metadata": self.extra_data,
|
"metadata": self.extra_data,
|
||||||
"collected_at": self.collected_at.isoformat()
|
"collected_at": self.collected_at.isoformat()
|
||||||
if self.collected_at is not None
|
if self.collected_at is not None
|
||||||
@@ -77,4 +80,9 @@ class CollectedData(Base):
|
|||||||
"reference_date": self.reference_date.isoformat()
|
"reference_date": self.reference_date.isoformat()
|
||||||
if self.reference_date is not None
|
if self.reference_date is not None
|
||||||
else None,
|
else None,
|
||||||
|
"is_current": self.is_current,
|
||||||
|
"previous_record_id": self.previous_record_id,
|
||||||
|
"change_type": self.change_type,
|
||||||
|
"change_summary": self.change_summary,
|
||||||
|
"deleted_at": self.deleted_at.isoformat() if self.deleted_at is not None else None,
|
||||||
}
|
}
|
||||||
|
|||||||
26
backend/app/models/data_snapshot.py
Normal file
26
backend/app/models/data_snapshot.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, JSON, String
|
||||||
|
from sqlalchemy.sql import func
|
||||||
|
|
||||||
|
from app.db.session import Base
|
||||||
|
|
||||||
|
|
||||||
|
class DataSnapshot(Base):
|
||||||
|
__tablename__ = "data_snapshots"
|
||||||
|
|
||||||
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
|
datasource_id = Column(Integer, nullable=False, index=True)
|
||||||
|
task_id = Column(Integer, ForeignKey("collection_tasks.id"), nullable=True, index=True)
|
||||||
|
source = Column(String(100), nullable=False, index=True)
|
||||||
|
snapshot_key = Column(String(100), nullable=True, index=True)
|
||||||
|
reference_date = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
started_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
completed_at = Column(DateTime(timezone=True), nullable=True)
|
||||||
|
record_count = Column(Integer, default=0)
|
||||||
|
status = Column(String(20), nullable=False, default="running")
|
||||||
|
is_current = Column(Boolean, default=True, index=True)
|
||||||
|
parent_snapshot_id = Column(Integer, ForeignKey("data_snapshots.id"), nullable=True, index=True)
|
||||||
|
summary = Column(JSON, default={})
|
||||||
|
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return f"<DataSnapshot {self.id}: {self.source}/{self.status}>"
|
||||||
@@ -12,6 +12,7 @@ class CollectionTask(Base):
|
|||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
datasource_id = Column(Integer, nullable=False, index=True)
|
datasource_id = Column(Integer, nullable=False, index=True)
|
||||||
status = Column(String(20), nullable=False) # pending, running, success, failed, cancelled
|
status = Column(String(20), nullable=False) # pending, running, success, failed, cancelled
|
||||||
|
phase = Column(String(30), default="queued")
|
||||||
started_at = Column(DateTime(timezone=True))
|
started_at = Column(DateTime(timezone=True))
|
||||||
completed_at = Column(DateTime(timezone=True))
|
completed_at = Column(DateTime(timezone=True))
|
||||||
records_processed = Column(Integer, default=0)
|
records_processed = Column(Integer, default=0)
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from typing import Dict, Any, List
|
import asyncio
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
from app.services.collectors.base import BaseCollector
|
|
||||||
from app.core.data_sources import get_data_sources_config
|
from app.core.data_sources import get_data_sources_config
|
||||||
|
from app.services.collectors.base import BaseCollector
|
||||||
|
|
||||||
|
|
||||||
class ArcGISCableLandingRelationCollector(BaseCollector):
|
class ArcGISCableLandingRelationCollector(BaseCollector):
|
||||||
@@ -18,45 +19,129 @@ class ArcGISCableLandingRelationCollector(BaseCollector):
|
|||||||
def base_url(self) -> str:
|
def base_url(self) -> str:
|
||||||
if self._resolved_url:
|
if self._resolved_url:
|
||||||
return self._resolved_url
|
return self._resolved_url
|
||||||
from app.core.data_sources import get_data_sources_config
|
|
||||||
|
|
||||||
config = get_data_sources_config()
|
config = get_data_sources_config()
|
||||||
return config.get_yaml_url("arcgis_cable_landing_relation")
|
return config.get_yaml_url("arcgis_cable_landing_relation")
|
||||||
|
|
||||||
async def fetch(self) -> List[Dict[str, Any]]:
|
def _layer_url(self, layer_id: int) -> str:
|
||||||
params = {"where": "1=1", "outFields": "*", "returnGeometry": "true", "f": "geojson"}
|
if "/FeatureServer/" not in self.base_url:
|
||||||
|
return self.base_url
|
||||||
|
prefix = self.base_url.split("/FeatureServer/")[0]
|
||||||
|
return f"{prefix}/FeatureServer/{layer_id}/query"
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
async def _fetch_layer_attributes(
|
||||||
response = await client.get(self.base_url, params=params)
|
self, client: httpx.AsyncClient, layer_id: int
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
response = await client.get(
|
||||||
|
self._layer_url(layer_id),
|
||||||
|
params={
|
||||||
|
"where": "1=1",
|
||||||
|
"outFields": "*",
|
||||||
|
"returnGeometry": "false",
|
||||||
|
"f": "json",
|
||||||
|
},
|
||||||
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return self.parse_response(response.json())
|
data = response.json()
|
||||||
|
return [feature.get("attributes", {}) for feature in data.get("features", [])]
|
||||||
|
|
||||||
def parse_response(self, data: Dict[str, Any]) -> List[Dict[str, Any]]:
|
async def _fetch_relation_features(self, client: httpx.AsyncClient) -> List[Dict[str, Any]]:
|
||||||
result = []
|
response = await client.get(
|
||||||
|
self.base_url,
|
||||||
|
params={
|
||||||
|
"where": "1=1",
|
||||||
|
"outFields": "*",
|
||||||
|
"returnGeometry": "true",
|
||||||
|
"f": "geojson",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
return data.get("features", [])
|
||||||
|
|
||||||
features = data.get("features", [])
|
async def fetch(self) -> List[Dict[str, Any]]:
|
||||||
for feature in features:
|
async with httpx.AsyncClient(timeout=60.0) as client:
|
||||||
|
relation_features, landing_rows, cable_rows = await asyncio.gather(
|
||||||
|
self._fetch_relation_features(client),
|
||||||
|
self._fetch_layer_attributes(client, 1),
|
||||||
|
self._fetch_layer_attributes(client, 2),
|
||||||
|
)
|
||||||
|
return self.parse_response(relation_features, landing_rows, cable_rows)
|
||||||
|
|
||||||
|
def _build_landing_lookup(self, landing_rows: List[Dict[str, Any]]) -> Dict[int, Dict[str, Any]]:
|
||||||
|
lookup: Dict[int, Dict[str, Any]] = {}
|
||||||
|
for row in landing_rows:
|
||||||
|
city_id = row.get("city_id")
|
||||||
|
if city_id is None:
|
||||||
|
continue
|
||||||
|
lookup[int(city_id)] = {
|
||||||
|
"landing_point_id": row.get("landing_point_id") or city_id,
|
||||||
|
"landing_point_name": row.get("Name") or row.get("name") or "",
|
||||||
|
"facility": row.get("facility") or "",
|
||||||
|
"status": row.get("status") or "",
|
||||||
|
"country": row.get("country") or "",
|
||||||
|
}
|
||||||
|
return lookup
|
||||||
|
|
||||||
|
def _build_cable_lookup(self, cable_rows: List[Dict[str, Any]]) -> Dict[int, Dict[str, Any]]:
|
||||||
|
lookup: Dict[int, Dict[str, Any]] = {}
|
||||||
|
for row in cable_rows:
|
||||||
|
cable_id = row.get("cable_id")
|
||||||
|
if cable_id is None:
|
||||||
|
continue
|
||||||
|
lookup[int(cable_id)] = {
|
||||||
|
"cable_name": row.get("Name") or "",
|
||||||
|
"status": row.get("status") or "active",
|
||||||
|
}
|
||||||
|
return lookup
|
||||||
|
|
||||||
|
def parse_response(
|
||||||
|
self,
|
||||||
|
relation_features: List[Dict[str, Any]],
|
||||||
|
landing_rows: List[Dict[str, Any]],
|
||||||
|
cable_rows: List[Dict[str, Any]],
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
result: List[Dict[str, Any]] = []
|
||||||
|
landing_lookup = self._build_landing_lookup(landing_rows)
|
||||||
|
cable_lookup = self._build_cable_lookup(cable_rows)
|
||||||
|
|
||||||
|
for feature in relation_features:
|
||||||
props = feature.get("properties", {})
|
props = feature.get("properties", {})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
city_id = props.get("city_id")
|
||||||
|
cable_id = props.get("cable_id")
|
||||||
|
landing_info = landing_lookup.get(int(city_id), {}) if city_id is not None else {}
|
||||||
|
cable_info = cable_lookup.get(int(cable_id), {}) if cable_id is not None else {}
|
||||||
|
|
||||||
|
cable_name = cable_info.get("cable_name") or props.get("cable_name") or "Unknown"
|
||||||
|
landing_point_name = (
|
||||||
|
landing_info.get("landing_point_name")
|
||||||
|
or props.get("landing_point_name")
|
||||||
|
or "Unknown"
|
||||||
|
)
|
||||||
|
facility = landing_info.get("facility") or props.get("facility") or "-"
|
||||||
|
status = cable_info.get("status") or landing_info.get("status") or props.get("status") or "-"
|
||||||
|
country = landing_info.get("country") or props.get("country") or ""
|
||||||
|
landing_point_id = landing_info.get("landing_point_id") or props.get("landing_point_id") or city_id
|
||||||
|
|
||||||
entry = {
|
entry = {
|
||||||
"source_id": f"arcgis_relation_{props.get('OBJECTID', props.get('id', ''))}",
|
"source_id": f"arcgis_relation_{props.get('OBJECTID', props.get('id', ''))}",
|
||||||
"name": f"{props.get('cable_name', 'Unknown')} - {props.get('landing_point_name', 'Unknown')}",
|
"name": f"{cable_name} - {landing_point_name}",
|
||||||
"country": props.get("country", ""),
|
"country": country,
|
||||||
"city": props.get("landing_point_name", ""),
|
"city": landing_point_name,
|
||||||
"latitude": str(props.get("latitude", "")) if props.get("latitude") else "",
|
"latitude": str(props.get("latitude", "")) if props.get("latitude") else "",
|
||||||
"longitude": str(props.get("longitude", "")) if props.get("longitude") else "",
|
"longitude": str(props.get("longitude", "")) if props.get("longitude") else "",
|
||||||
"value": "",
|
"value": "",
|
||||||
"unit": "",
|
"unit": "",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"objectid": props.get("OBJECTID"),
|
"objectid": props.get("OBJECTID"),
|
||||||
"city_id": props.get("city_id"),
|
"city_id": city_id,
|
||||||
"cable_id": props.get("cable_id"),
|
"cable_id": cable_id,
|
||||||
"cable_name": props.get("cable_name"),
|
"cable_name": cable_name,
|
||||||
"landing_point_id": props.get("landing_point_id"),
|
"landing_point_id": landing_point_id,
|
||||||
"landing_point_name": props.get("landing_point_name"),
|
"landing_point_name": landing_point_name,
|
||||||
"facility": props.get("facility"),
|
"facility": facility,
|
||||||
"status": props.get("status"),
|
"status": status,
|
||||||
},
|
},
|
||||||
"reference_date": datetime.utcnow().strftime("%Y-%m-%d"),
|
"reference_date": datetime.utcnow().strftime("%Y-%m-%d"),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,10 +4,12 @@ from abc import ABC, abstractmethod
|
|||||||
from typing import Dict, List, Any, Optional
|
from typing import Dict, List, Any, Optional
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import httpx
|
import httpx
|
||||||
from sqlalchemy import text
|
from sqlalchemy import select, text
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.core.collected_data_fields import build_dynamic_metadata, get_record_field
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
|
from app.core.countries import normalize_country
|
||||||
|
|
||||||
|
|
||||||
class BaseCollector(ABC):
|
class BaseCollector(ABC):
|
||||||
@@ -39,6 +41,11 @@ class BaseCollector(ABC):
|
|||||||
records_processed / self._current_task.total_records
|
records_processed / self._current_task.total_records
|
||||||
) * 100
|
) * 100
|
||||||
|
|
||||||
|
async def set_phase(self, phase: str):
|
||||||
|
if self._current_task and self._db_session:
|
||||||
|
self._current_task.phase = phase
|
||||||
|
await self._db_session.commit()
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def fetch(self) -> List[Dict[str, Any]]:
|
async def fetch(self) -> List[Dict[str, Any]]:
|
||||||
"""Fetch raw data from source"""
|
"""Fetch raw data from source"""
|
||||||
@@ -48,14 +55,87 @@ class BaseCollector(ABC):
|
|||||||
"""Transform raw data to internal format (default: pass through)"""
|
"""Transform raw data to internal format (default: pass through)"""
|
||||||
return raw_data
|
return raw_data
|
||||||
|
|
||||||
|
def _parse_reference_date(self, value: Any) -> Optional[datetime]:
|
||||||
|
if not value:
|
||||||
|
return None
|
||||||
|
if isinstance(value, datetime):
|
||||||
|
return value
|
||||||
|
if isinstance(value, str):
|
||||||
|
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _build_comparable_payload(self, record: Any) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
"name": getattr(record, "name", None),
|
||||||
|
"title": getattr(record, "title", None),
|
||||||
|
"description": getattr(record, "description", None),
|
||||||
|
"country": get_record_field(record, "country"),
|
||||||
|
"city": get_record_field(record, "city"),
|
||||||
|
"latitude": get_record_field(record, "latitude"),
|
||||||
|
"longitude": get_record_field(record, "longitude"),
|
||||||
|
"value": get_record_field(record, "value"),
|
||||||
|
"unit": get_record_field(record, "unit"),
|
||||||
|
"metadata": getattr(record, "extra_data", None) or {},
|
||||||
|
"reference_date": (
|
||||||
|
getattr(record, "reference_date", None).isoformat()
|
||||||
|
if getattr(record, "reference_date", None)
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
async def _create_snapshot(
|
||||||
|
self,
|
||||||
|
db: AsyncSession,
|
||||||
|
task_id: int,
|
||||||
|
data: List[Dict[str, Any]],
|
||||||
|
started_at: datetime,
|
||||||
|
) -> int:
|
||||||
|
from app.models.data_snapshot import DataSnapshot
|
||||||
|
|
||||||
|
reference_dates = [
|
||||||
|
parsed
|
||||||
|
for parsed in (self._parse_reference_date(item.get("reference_date")) for item in data)
|
||||||
|
if parsed is not None
|
||||||
|
]
|
||||||
|
reference_date = max(reference_dates) if reference_dates else None
|
||||||
|
|
||||||
|
result = await db.execute(
|
||||||
|
select(DataSnapshot)
|
||||||
|
.where(DataSnapshot.source == self.name, DataSnapshot.is_current == True)
|
||||||
|
.order_by(DataSnapshot.completed_at.desc().nullslast(), DataSnapshot.id.desc())
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
previous_snapshot = result.scalar_one_or_none()
|
||||||
|
|
||||||
|
snapshot = DataSnapshot(
|
||||||
|
datasource_id=getattr(self, "_datasource_id", 1),
|
||||||
|
task_id=task_id,
|
||||||
|
source=self.name,
|
||||||
|
snapshot_key=f"{self.name}:{task_id}",
|
||||||
|
reference_date=reference_date,
|
||||||
|
started_at=started_at,
|
||||||
|
status="running",
|
||||||
|
is_current=True,
|
||||||
|
parent_snapshot_id=previous_snapshot.id if previous_snapshot else None,
|
||||||
|
summary={},
|
||||||
|
)
|
||||||
|
db.add(snapshot)
|
||||||
|
|
||||||
|
if previous_snapshot:
|
||||||
|
previous_snapshot.is_current = False
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
|
return snapshot.id
|
||||||
|
|
||||||
async def run(self, db: AsyncSession) -> Dict[str, Any]:
|
async def run(self, db: AsyncSession) -> Dict[str, Any]:
|
||||||
"""Full pipeline: fetch -> transform -> save"""
|
"""Full pipeline: fetch -> transform -> save"""
|
||||||
from app.services.collectors.registry import collector_registry
|
from app.services.collectors.registry import collector_registry
|
||||||
from app.models.task import CollectionTask
|
from app.models.task import CollectionTask
|
||||||
from app.models.collected_data import CollectedData
|
from app.models.data_snapshot import DataSnapshot
|
||||||
|
|
||||||
start_time = datetime.utcnow()
|
start_time = datetime.utcnow()
|
||||||
datasource_id = getattr(self, "_datasource_id", 1)
|
datasource_id = getattr(self, "_datasource_id", 1)
|
||||||
|
snapshot_id: Optional[int] = None
|
||||||
|
|
||||||
if not collector_registry.is_active(self.name):
|
if not collector_registry.is_active(self.name):
|
||||||
return {"status": "skipped", "reason": "Collector is disabled"}
|
return {"status": "skipped", "reason": "Collector is disabled"}
|
||||||
@@ -63,6 +143,7 @@ class BaseCollector(ABC):
|
|||||||
task = CollectionTask(
|
task = CollectionTask(
|
||||||
datasource_id=datasource_id,
|
datasource_id=datasource_id,
|
||||||
status="running",
|
status="running",
|
||||||
|
phase="queued",
|
||||||
started_at=start_time,
|
started_at=start_time,
|
||||||
)
|
)
|
||||||
db.add(task)
|
db.add(task)
|
||||||
@@ -75,15 +156,20 @@ class BaseCollector(ABC):
|
|||||||
await self.resolve_url(db)
|
await self.resolve_url(db)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
await self.set_phase("fetching")
|
||||||
raw_data = await self.fetch()
|
raw_data = await self.fetch()
|
||||||
task.total_records = len(raw_data)
|
task.total_records = len(raw_data)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
|
await self.set_phase("transforming")
|
||||||
data = self.transform(raw_data)
|
data = self.transform(raw_data)
|
||||||
|
snapshot_id = await self._create_snapshot(db, task_id, data, start_time)
|
||||||
|
|
||||||
records_count = await self._save_data(db, data)
|
await self.set_phase("saving")
|
||||||
|
records_count = await self._save_data(db, data, task_id=task_id, snapshot_id=snapshot_id)
|
||||||
|
|
||||||
task.status = "success"
|
task.status = "success"
|
||||||
|
task.phase = "completed"
|
||||||
task.records_processed = records_count
|
task.records_processed = records_count
|
||||||
task.progress = 100.0
|
task.progress = 100.0
|
||||||
task.completed_at = datetime.utcnow()
|
task.completed_at = datetime.utcnow()
|
||||||
@@ -97,8 +183,15 @@ class BaseCollector(ABC):
|
|||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
task.status = "failed"
|
task.status = "failed"
|
||||||
|
task.phase = "failed"
|
||||||
task.error_message = str(e)
|
task.error_message = str(e)
|
||||||
task.completed_at = datetime.utcnow()
|
task.completed_at = datetime.utcnow()
|
||||||
|
if snapshot_id is not None:
|
||||||
|
snapshot = await db.get(DataSnapshot, snapshot_id)
|
||||||
|
if snapshot:
|
||||||
|
snapshot.status = "failed"
|
||||||
|
snapshot.completed_at = datetime.utcnow()
|
||||||
|
snapshot.summary = {"error": str(e)}
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -108,53 +201,163 @@ class BaseCollector(ABC):
|
|||||||
"execution_time_seconds": (datetime.utcnow() - start_time).total_seconds(),
|
"execution_time_seconds": (datetime.utcnow() - start_time).total_seconds(),
|
||||||
}
|
}
|
||||||
|
|
||||||
async def _save_data(self, db: AsyncSession, data: List[Dict[str, Any]]) -> int:
|
async def _save_data(
|
||||||
|
self,
|
||||||
|
db: AsyncSession,
|
||||||
|
data: List[Dict[str, Any]],
|
||||||
|
task_id: Optional[int] = None,
|
||||||
|
snapshot_id: Optional[int] = None,
|
||||||
|
) -> int:
|
||||||
"""Save transformed data to database"""
|
"""Save transformed data to database"""
|
||||||
from app.models.collected_data import CollectedData
|
from app.models.collected_data import CollectedData
|
||||||
|
from app.models.data_snapshot import DataSnapshot
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
|
if snapshot_id is not None:
|
||||||
|
snapshot = await db.get(DataSnapshot, snapshot_id)
|
||||||
|
if snapshot:
|
||||||
|
snapshot.record_count = 0
|
||||||
|
snapshot.summary = {"created": 0, "updated": 0, "unchanged": 0}
|
||||||
|
snapshot.status = "success"
|
||||||
|
snapshot.completed_at = datetime.utcnow()
|
||||||
|
await db.commit()
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
collected_at = datetime.utcnow()
|
collected_at = datetime.utcnow()
|
||||||
records_added = 0
|
records_added = 0
|
||||||
|
created_count = 0
|
||||||
|
updated_count = 0
|
||||||
|
unchanged_count = 0
|
||||||
|
seen_entity_keys: set[str] = set()
|
||||||
|
previous_current_keys: set[str] = set()
|
||||||
|
|
||||||
|
previous_current_result = await db.execute(
|
||||||
|
select(CollectedData.entity_key).where(
|
||||||
|
CollectedData.source == self.name,
|
||||||
|
CollectedData.is_current == True,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
previous_current_keys = {row[0] for row in previous_current_result.fetchall() if row[0]}
|
||||||
|
|
||||||
for i, item in enumerate(data):
|
for i, item in enumerate(data):
|
||||||
print(
|
print(
|
||||||
f"DEBUG: Saving item {i}: name={item.get('name')}, metadata={item.get('metadata', 'NOT FOUND')}"
|
f"DEBUG: Saving item {i}: name={item.get('name')}, metadata={item.get('metadata', 'NOT FOUND')}"
|
||||||
)
|
)
|
||||||
|
raw_metadata = item.get("metadata", {})
|
||||||
|
extra_data = build_dynamic_metadata(
|
||||||
|
raw_metadata,
|
||||||
|
country=item.get("country"),
|
||||||
|
city=item.get("city"),
|
||||||
|
latitude=item.get("latitude"),
|
||||||
|
longitude=item.get("longitude"),
|
||||||
|
value=item.get("value"),
|
||||||
|
unit=item.get("unit"),
|
||||||
|
)
|
||||||
|
normalized_country = normalize_country(item.get("country"))
|
||||||
|
if normalized_country is not None:
|
||||||
|
extra_data["country"] = normalized_country
|
||||||
|
|
||||||
|
if item.get("country") and normalized_country != item.get("country"):
|
||||||
|
extra_data["raw_country"] = item.get("country")
|
||||||
|
if normalized_country is None:
|
||||||
|
extra_data["country_validation"] = "invalid"
|
||||||
|
|
||||||
|
source_id = item.get("source_id") or item.get("id")
|
||||||
|
reference_date = (
|
||||||
|
self._parse_reference_date(item.get("reference_date"))
|
||||||
|
)
|
||||||
|
source_id_str = str(source_id) if source_id is not None else None
|
||||||
|
entity_key = f"{self.name}:{source_id_str}" if source_id_str else f"{self.name}:{i}"
|
||||||
|
previous_record = None
|
||||||
|
|
||||||
|
if entity_key and entity_key not in seen_entity_keys:
|
||||||
|
result = await db.execute(
|
||||||
|
select(CollectedData)
|
||||||
|
.where(
|
||||||
|
CollectedData.source == self.name,
|
||||||
|
CollectedData.entity_key == entity_key,
|
||||||
|
CollectedData.is_current == True,
|
||||||
|
)
|
||||||
|
.order_by(CollectedData.collected_at.desc().nullslast(), CollectedData.id.desc())
|
||||||
|
)
|
||||||
|
previous_records = result.scalars().all()
|
||||||
|
if previous_records:
|
||||||
|
previous_record = previous_records[0]
|
||||||
|
for old_record in previous_records:
|
||||||
|
old_record.is_current = False
|
||||||
|
|
||||||
record = CollectedData(
|
record = CollectedData(
|
||||||
|
snapshot_id=snapshot_id,
|
||||||
|
task_id=task_id,
|
||||||
source=self.name,
|
source=self.name,
|
||||||
source_id=item.get("source_id") or item.get("id"),
|
source_id=source_id_str,
|
||||||
|
entity_key=entity_key,
|
||||||
data_type=self.data_type,
|
data_type=self.data_type,
|
||||||
name=item.get("name"),
|
name=item.get("name"),
|
||||||
title=item.get("title"),
|
title=item.get("title"),
|
||||||
description=item.get("description"),
|
description=item.get("description"),
|
||||||
country=item.get("country"),
|
extra_data=extra_data,
|
||||||
city=item.get("city"),
|
|
||||||
latitude=str(item.get("latitude", ""))
|
|
||||||
if item.get("latitude") is not None
|
|
||||||
else None,
|
|
||||||
longitude=str(item.get("longitude", ""))
|
|
||||||
if item.get("longitude") is not None
|
|
||||||
else None,
|
|
||||||
value=item.get("value"),
|
|
||||||
unit=item.get("unit"),
|
|
||||||
extra_data=item.get("metadata", {}),
|
|
||||||
collected_at=collected_at,
|
collected_at=collected_at,
|
||||||
reference_date=datetime.fromisoformat(
|
reference_date=reference_date,
|
||||||
item.get("reference_date").replace("Z", "+00:00")
|
|
||||||
)
|
|
||||||
if item.get("reference_date")
|
|
||||||
else None,
|
|
||||||
is_valid=1,
|
is_valid=1,
|
||||||
|
is_current=True,
|
||||||
|
previous_record_id=previous_record.id if previous_record else None,
|
||||||
|
deleted_at=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if previous_record is None:
|
||||||
|
record.change_type = "created"
|
||||||
|
record.change_summary = {}
|
||||||
|
created_count += 1
|
||||||
|
else:
|
||||||
|
previous_payload = self._build_comparable_payload(previous_record)
|
||||||
|
current_payload = self._build_comparable_payload(record)
|
||||||
|
if current_payload == previous_payload:
|
||||||
|
record.change_type = "unchanged"
|
||||||
|
record.change_summary = {}
|
||||||
|
unchanged_count += 1
|
||||||
|
else:
|
||||||
|
changed_fields = [
|
||||||
|
key for key in current_payload.keys() if current_payload[key] != previous_payload.get(key)
|
||||||
|
]
|
||||||
|
record.change_type = "updated"
|
||||||
|
record.change_summary = {"changed_fields": changed_fields}
|
||||||
|
updated_count += 1
|
||||||
|
|
||||||
db.add(record)
|
db.add(record)
|
||||||
|
seen_entity_keys.add(entity_key)
|
||||||
records_added += 1
|
records_added += 1
|
||||||
|
|
||||||
if i % 100 == 0:
|
if i % 100 == 0:
|
||||||
self.update_progress(i + 1)
|
self.update_progress(i + 1)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
|
if snapshot_id is not None:
|
||||||
|
deleted_keys = previous_current_keys - seen_entity_keys
|
||||||
|
await db.execute(
|
||||||
|
text(
|
||||||
|
"""
|
||||||
|
UPDATE collected_data
|
||||||
|
SET is_current = FALSE
|
||||||
|
WHERE source = :source
|
||||||
|
AND snapshot_id IS DISTINCT FROM :snapshot_id
|
||||||
|
AND COALESCE(is_current, TRUE) = TRUE
|
||||||
|
"""
|
||||||
|
),
|
||||||
|
{"source": self.name, "snapshot_id": snapshot_id},
|
||||||
|
)
|
||||||
|
snapshot = await db.get(DataSnapshot, snapshot_id)
|
||||||
|
if snapshot:
|
||||||
|
snapshot.record_count = records_added
|
||||||
|
snapshot.status = "success"
|
||||||
|
snapshot.completed_at = datetime.utcnow()
|
||||||
|
snapshot.summary = {
|
||||||
|
"created": created_count,
|
||||||
|
"updated": updated_count,
|
||||||
|
"unchanged": unchanged_count,
|
||||||
|
"deleted": len(deleted_keys),
|
||||||
|
}
|
||||||
|
|
||||||
await db.commit()
|
await db.commit()
|
||||||
self.update_progress(len(data))
|
self.update_progress(len(data))
|
||||||
return records_added
|
return records_added
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ class PeeringDBIXPCollector(HTTPCollector):
|
|||||||
print(f"Warning: PeeringDB collection failed after {max_retries} retries: {last_error}")
|
print(f"Warning: PeeringDB collection failed after {max_retries} retries: {last_error}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
async def collect(self) -> List[Dict[str, Any]]:
|
async def fetch(self) -> List[Dict[str, Any]]:
|
||||||
"""Collect IXP data from PeeringDB with rate limit handling"""
|
"""Collect IXP data from PeeringDB with rate limit handling"""
|
||||||
response_data = await self.fetch_with_retry()
|
response_data = await self.fetch_with_retry()
|
||||||
if not response_data:
|
if not response_data:
|
||||||
@@ -177,7 +177,7 @@ class PeeringDBNetworkCollector(HTTPCollector):
|
|||||||
print(f"Warning: PeeringDB collection failed after {max_retries} retries: {last_error}")
|
print(f"Warning: PeeringDB collection failed after {max_retries} retries: {last_error}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
async def collect(self) -> List[Dict[str, Any]]:
|
async def fetch(self) -> List[Dict[str, Any]]:
|
||||||
"""Collect Network data from PeeringDB with rate limit handling"""
|
"""Collect Network data from PeeringDB with rate limit handling"""
|
||||||
response_data = await self.fetch_with_retry()
|
response_data = await self.fetch_with_retry()
|
||||||
if not response_data:
|
if not response_data:
|
||||||
@@ -280,7 +280,7 @@ class PeeringDBFacilityCollector(HTTPCollector):
|
|||||||
print(f"Warning: PeeringDB collection failed after {max_retries} retries: {last_error}")
|
print(f"Warning: PeeringDB collection failed after {max_retries} retries: {last_error}")
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
async def collect(self) -> List[Dict[str, Any]]:
|
async def fetch(self) -> List[Dict[str, Any]]:
|
||||||
"""Collect Facility data from PeeringDB with rate limit handling"""
|
"""Collect Facility data from PeeringDB with rate limit handling"""
|
||||||
response_data = await self.fetch_with_retry()
|
response_data = await self.fetch_with_retry()
|
||||||
if not response_data:
|
if not response_data:
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ Collects data from TOP500 supercomputer rankings.
|
|||||||
https://top500.org/lists/top500/
|
https://top500.org/lists/top500/
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
import re
|
import re
|
||||||
from typing import Dict, Any, List
|
from typing import Dict, Any, List
|
||||||
from datetime import datetime
|
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
import httpx
|
import httpx
|
||||||
|
|
||||||
@@ -21,14 +21,108 @@ class TOP500Collector(BaseCollector):
|
|||||||
data_type = "supercomputer"
|
data_type = "supercomputer"
|
||||||
|
|
||||||
async def fetch(self) -> List[Dict[str, Any]]:
|
async def fetch(self) -> List[Dict[str, Any]]:
|
||||||
"""Fetch TOP500 data from website (scraping)"""
|
"""Fetch TOP500 list data and enrich each row with detail-page metadata."""
|
||||||
# Get the latest list page
|
|
||||||
url = "https://top500.org/lists/top500/list/2025/11/"
|
url = "https://top500.org/lists/top500/list/2025/11/"
|
||||||
|
|
||||||
async with httpx.AsyncClient(timeout=60.0) as client:
|
async with httpx.AsyncClient(timeout=60.0, follow_redirects=True) as client:
|
||||||
response = await client.get(url)
|
response = await client.get(url)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
return self.parse_response(response.text)
|
entries = self.parse_response(response.text)
|
||||||
|
|
||||||
|
semaphore = asyncio.Semaphore(8)
|
||||||
|
|
||||||
|
async def enrich(entry: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
detail_url = entry.pop("_detail_url", "")
|
||||||
|
if not detail_url:
|
||||||
|
return entry
|
||||||
|
|
||||||
|
async with semaphore:
|
||||||
|
try:
|
||||||
|
detail_response = await client.get(detail_url)
|
||||||
|
detail_response.raise_for_status()
|
||||||
|
entry["metadata"].update(self.parse_detail_response(detail_response.text))
|
||||||
|
except Exception:
|
||||||
|
entry["metadata"]["detail_fetch_failed"] = True
|
||||||
|
return entry
|
||||||
|
|
||||||
|
return await asyncio.gather(*(enrich(entry) for entry in entries))
|
||||||
|
|
||||||
|
def _extract_system_fields(self, system_cell) -> Dict[str, str]:
|
||||||
|
link = system_cell.find("a")
|
||||||
|
system_name = link.get_text(" ", strip=True) if link else system_cell.get_text(" ", strip=True)
|
||||||
|
detail_url = ""
|
||||||
|
if link and link.get("href"):
|
||||||
|
detail_url = f"https://top500.org{link.get('href')}"
|
||||||
|
|
||||||
|
manufacturer = ""
|
||||||
|
if link and link.next_sibling:
|
||||||
|
manufacturer = str(link.next_sibling).strip(" ,\n\t")
|
||||||
|
|
||||||
|
cell_text = system_cell.get_text("\n", strip=True)
|
||||||
|
lines = [line.strip(" ,") for line in cell_text.splitlines() if line.strip()]
|
||||||
|
|
||||||
|
site = ""
|
||||||
|
country = ""
|
||||||
|
if lines:
|
||||||
|
system_name = lines[0]
|
||||||
|
if len(lines) >= 3:
|
||||||
|
site = lines[-2]
|
||||||
|
country = lines[-1]
|
||||||
|
elif len(lines) == 2:
|
||||||
|
country = lines[-1]
|
||||||
|
|
||||||
|
if not manufacturer and len(lines) >= 2:
|
||||||
|
manufacturer = lines[1]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"name": system_name,
|
||||||
|
"manufacturer": manufacturer,
|
||||||
|
"site": site,
|
||||||
|
"country": country,
|
||||||
|
"detail_url": detail_url,
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse_detail_response(self, html: str) -> Dict[str, Any]:
|
||||||
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
|
detail_table = soup.find("table", {"class": "table table-condensed"})
|
||||||
|
if not detail_table:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
detail_map: Dict[str, Any] = {}
|
||||||
|
label_aliases = {
|
||||||
|
"Site": "site",
|
||||||
|
"Manufacturer": "manufacturer",
|
||||||
|
"Cores": "cores",
|
||||||
|
"Processor": "processor",
|
||||||
|
"Interconnect": "interconnect",
|
||||||
|
"Installation Year": "installation_year",
|
||||||
|
"Linpack Performance (Rmax)": "rmax",
|
||||||
|
"Theoretical Peak (Rpeak)": "rpeak",
|
||||||
|
"Nmax": "nmax",
|
||||||
|
"HPCG": "hpcg",
|
||||||
|
"Power": "power",
|
||||||
|
"Power Measurement Level": "power_measurement_level",
|
||||||
|
"Operating System": "operating_system",
|
||||||
|
"Compiler": "compiler",
|
||||||
|
"Math Library": "math_library",
|
||||||
|
"MPI": "mpi",
|
||||||
|
}
|
||||||
|
|
||||||
|
for row in detail_table.find_all("tr"):
|
||||||
|
header = row.find("th")
|
||||||
|
value_cell = row.find("td")
|
||||||
|
if not header or not value_cell:
|
||||||
|
continue
|
||||||
|
|
||||||
|
label = header.get_text(" ", strip=True).rstrip(":")
|
||||||
|
key = label_aliases.get(label)
|
||||||
|
if not key:
|
||||||
|
continue
|
||||||
|
|
||||||
|
value = value_cell.get_text(" ", strip=True)
|
||||||
|
detail_map[key] = value
|
||||||
|
|
||||||
|
return detail_map
|
||||||
|
|
||||||
def parse_response(self, html: str) -> List[Dict[str, Any]]:
|
def parse_response(self, html: str) -> List[Dict[str, Any]]:
|
||||||
"""Parse TOP500 HTML response"""
|
"""Parse TOP500 HTML response"""
|
||||||
@@ -36,27 +130,26 @@ class TOP500Collector(BaseCollector):
|
|||||||
soup = BeautifulSoup(html, "html.parser")
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
|
|
||||||
# Find the table with TOP500 data
|
# Find the table with TOP500 data
|
||||||
table = soup.find("table", {"class": "top500-table"})
|
table = None
|
||||||
if not table:
|
for candidate in soup.find_all("table"):
|
||||||
# Try alternative table selector
|
header_cells = [
|
||||||
table = soup.find("table", {"id": "top500"})
|
cell.get_text(" ", strip=True) for cell in candidate.select("thead th")
|
||||||
|
]
|
||||||
if not table:
|
normalized_headers = [header.lower() for header in header_cells]
|
||||||
# Try to find any table with rank data
|
if (
|
||||||
tables = soup.find_all("table")
|
"rank" in normalized_headers
|
||||||
for t in tables:
|
and "system" in normalized_headers
|
||||||
if t.find(string=re.compile(r"Rank.*System.*Cores.*Rmax", re.I)):
|
and any("cores" in header for header in normalized_headers)
|
||||||
table = t
|
and any("rmax" in header for header in normalized_headers)
|
||||||
|
):
|
||||||
|
table = candidate
|
||||||
break
|
break
|
||||||
|
|
||||||
if not table:
|
if not table:
|
||||||
# Fallback: try to extract data from any table
|
table = soup.find("table", {"class": "top500-table"}) or soup.find("table", {"id": "top500"})
|
||||||
tables = soup.find_all("table")
|
|
||||||
if tables:
|
|
||||||
table = tables[0]
|
|
||||||
|
|
||||||
if table:
|
if table:
|
||||||
rows = table.find_all("tr")
|
rows = table.select("tr")
|
||||||
for row in rows[1:]: # Skip header row
|
for row in rows[1:]: # Skip header row
|
||||||
cells = row.find_all(["td", "th"])
|
cells = row.find_all(["td", "th"])
|
||||||
if len(cells) >= 6:
|
if len(cells) >= 6:
|
||||||
@@ -68,43 +161,26 @@ class TOP500Collector(BaseCollector):
|
|||||||
|
|
||||||
rank = int(rank_text)
|
rank = int(rank_text)
|
||||||
|
|
||||||
# System name (may contain link)
|
|
||||||
system_cell = cells[1]
|
system_cell = cells[1]
|
||||||
system_name = system_cell.get_text(strip=True)
|
system_fields = self._extract_system_fields(system_cell)
|
||||||
# Try to get full name from link title or data attribute
|
system_name = system_fields["name"]
|
||||||
link = system_cell.find("a")
|
manufacturer = system_fields["manufacturer"]
|
||||||
if link and link.get("title"):
|
site = system_fields["site"]
|
||||||
system_name = link.get("title")
|
country = system_fields["country"]
|
||||||
|
detail_url = system_fields["detail_url"]
|
||||||
|
|
||||||
# Country
|
|
||||||
country_cell = cells[2]
|
|
||||||
country = country_cell.get_text(strip=True)
|
|
||||||
# Try to get country from data attribute or image alt
|
|
||||||
img = country_cell.find("img")
|
|
||||||
if img and img.get("alt"):
|
|
||||||
country = img.get("alt")
|
|
||||||
|
|
||||||
# Extract location (city)
|
|
||||||
city = ""
|
city = ""
|
||||||
location_text = country_cell.get_text(strip=True)
|
cores = cells[2].get_text(strip=True).replace(",", "")
|
||||||
if "(" in location_text and ")" in location_text:
|
|
||||||
city = location_text.split("(")[0].strip()
|
|
||||||
|
|
||||||
# Cores
|
rmax_text = cells[3].get_text(strip=True)
|
||||||
cores = cells[3].get_text(strip=True).replace(",", "")
|
|
||||||
|
|
||||||
# Rmax
|
|
||||||
rmax_text = cells[4].get_text(strip=True)
|
|
||||||
rmax = self._parse_performance(rmax_text)
|
rmax = self._parse_performance(rmax_text)
|
||||||
|
|
||||||
# Rpeak
|
rpeak_text = cells[4].get_text(strip=True)
|
||||||
rpeak_text = cells[5].get_text(strip=True)
|
|
||||||
rpeak = self._parse_performance(rpeak_text)
|
rpeak = self._parse_performance(rpeak_text)
|
||||||
|
|
||||||
# Power (optional)
|
|
||||||
power = ""
|
power = ""
|
||||||
if len(cells) >= 7:
|
if len(cells) >= 6:
|
||||||
power = cells[6].get_text(strip=True)
|
power = cells[5].get_text(strip=True).replace(",", "")
|
||||||
|
|
||||||
entry = {
|
entry = {
|
||||||
"source_id": f"top500_{rank}",
|
"source_id": f"top500_{rank}",
|
||||||
@@ -117,10 +193,14 @@ class TOP500Collector(BaseCollector):
|
|||||||
"unit": "PFlop/s",
|
"unit": "PFlop/s",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"rank": rank,
|
"rank": rank,
|
||||||
"r_peak": rpeak,
|
|
||||||
"power": power,
|
|
||||||
"cores": cores,
|
"cores": cores,
|
||||||
|
"rmax": rmax_text,
|
||||||
|
"rpeak": rpeak_text,
|
||||||
|
"power": power,
|
||||||
|
"manufacturer": manufacturer,
|
||||||
|
"site": site,
|
||||||
},
|
},
|
||||||
|
"_detail_url": detail_url,
|
||||||
"reference_date": "2025-11-01",
|
"reference_date": "2025-11-01",
|
||||||
}
|
}
|
||||||
data.append(entry)
|
data.append(entry)
|
||||||
@@ -184,10 +264,15 @@ class TOP500Collector(BaseCollector):
|
|||||||
"unit": "PFlop/s",
|
"unit": "PFlop/s",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"rank": 1,
|
"rank": 1,
|
||||||
"r_peak": 2746.38,
|
"cores": "11039616",
|
||||||
"power": 29581,
|
"rmax": "1742.00",
|
||||||
"cores": 11039616,
|
"rpeak": "2746.38",
|
||||||
|
"power": "29581",
|
||||||
"manufacturer": "HPE",
|
"manufacturer": "HPE",
|
||||||
|
"site": "DOE/NNSA/LLNL",
|
||||||
|
"processor": "AMD 4th Gen EPYC 24C 1.8GHz",
|
||||||
|
"interconnect": "Slingshot-11",
|
||||||
|
"installation_year": "2025",
|
||||||
},
|
},
|
||||||
"reference_date": "2025-11-01",
|
"reference_date": "2025-11-01",
|
||||||
},
|
},
|
||||||
@@ -202,10 +287,12 @@ class TOP500Collector(BaseCollector):
|
|||||||
"unit": "PFlop/s",
|
"unit": "PFlop/s",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"rank": 2,
|
"rank": 2,
|
||||||
"r_peak": 2055.72,
|
"cores": "9066176",
|
||||||
"power": 24607,
|
"rmax": "1353.00",
|
||||||
"cores": 9066176,
|
"rpeak": "2055.72",
|
||||||
|
"power": "24607",
|
||||||
"manufacturer": "HPE",
|
"manufacturer": "HPE",
|
||||||
|
"site": "DOE/SC/Oak Ridge National Laboratory",
|
||||||
},
|
},
|
||||||
"reference_date": "2025-11-01",
|
"reference_date": "2025-11-01",
|
||||||
},
|
},
|
||||||
@@ -220,9 +307,10 @@ class TOP500Collector(BaseCollector):
|
|||||||
"unit": "PFlop/s",
|
"unit": "PFlop/s",
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"rank": 3,
|
"rank": 3,
|
||||||
"r_peak": 1980.01,
|
"cores": "9264128",
|
||||||
"power": 38698,
|
"rmax": "1012.00",
|
||||||
"cores": 9264128,
|
"rpeak": "1980.01",
|
||||||
|
"power": "38698",
|
||||||
"manufacturer": "Intel",
|
"manufacturer": "Intel",
|
||||||
},
|
},
|
||||||
"reference_date": "2025-11-01",
|
"reference_date": "2025-11-01",
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||||
from apscheduler.triggers.interval import IntervalTrigger
|
from apscheduler.triggers.interval import IntervalTrigger
|
||||||
@@ -11,6 +11,7 @@ from sqlalchemy import select
|
|||||||
|
|
||||||
from app.db.session import async_session_factory
|
from app.db.session import async_session_factory
|
||||||
from app.models.datasource import DataSource
|
from app.models.datasource import DataSource
|
||||||
|
from app.models.task import CollectionTask
|
||||||
from app.services.collectors.registry import collector_registry
|
from app.services.collectors.registry import collector_registry
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@@ -89,6 +90,35 @@ async def run_collector_task(collector_name: str):
|
|||||||
logger.exception("Collector %s failed: %s", collector_name, exc)
|
logger.exception("Collector %s failed: %s", collector_name, exc)
|
||||||
|
|
||||||
|
|
||||||
|
async def cleanup_stale_running_tasks(max_age_hours: int = 2) -> int:
|
||||||
|
"""Mark stale running tasks as failed after restarts or collector hangs."""
|
||||||
|
cutoff = datetime.utcnow() - timedelta(hours=max_age_hours)
|
||||||
|
|
||||||
|
async with async_session_factory() as db:
|
||||||
|
result = await db.execute(
|
||||||
|
select(CollectionTask).where(
|
||||||
|
CollectionTask.status == "running",
|
||||||
|
CollectionTask.started_at.is_not(None),
|
||||||
|
CollectionTask.started_at < cutoff,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
stale_tasks = result.scalars().all()
|
||||||
|
|
||||||
|
for task in stale_tasks:
|
||||||
|
task.status = "failed"
|
||||||
|
task.phase = "failed"
|
||||||
|
task.completed_at = datetime.utcnow()
|
||||||
|
existing_error = (task.error_message or "").strip()
|
||||||
|
cleanup_error = "Marked failed automatically after stale running task cleanup"
|
||||||
|
task.error_message = f"{existing_error}\n{cleanup_error}".strip() if existing_error else cleanup_error
|
||||||
|
|
||||||
|
if stale_tasks:
|
||||||
|
await db.commit()
|
||||||
|
logger.warning("Cleaned up %s stale running collection task(s)", len(stale_tasks))
|
||||||
|
|
||||||
|
return len(stale_tasks)
|
||||||
|
|
||||||
|
|
||||||
def start_scheduler() -> None:
|
def start_scheduler() -> None:
|
||||||
"""Start the scheduler."""
|
"""Start the scheduler."""
|
||||||
if not scheduler.running:
|
if not scheduler.running:
|
||||||
@@ -144,6 +174,19 @@ def get_scheduler_jobs() -> list[Dict[str, Any]]:
|
|||||||
return jobs
|
return jobs
|
||||||
|
|
||||||
|
|
||||||
|
async def get_latest_task_id_for_datasource(datasource_id: int) -> Optional[int]:
|
||||||
|
from app.models.task import CollectionTask
|
||||||
|
|
||||||
|
async with async_session_factory() as db:
|
||||||
|
result = await db.execute(
|
||||||
|
select(CollectionTask.id)
|
||||||
|
.where(CollectionTask.datasource_id == datasource_id)
|
||||||
|
.order_by(CollectionTask.created_at.desc(), CollectionTask.id.desc())
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
return result.scalar_one_or_none()
|
||||||
|
|
||||||
|
|
||||||
def run_collector_now(collector_name: str) -> bool:
|
def run_collector_now(collector_name: str) -> bool:
|
||||||
"""Run a collector immediately (not scheduled)."""
|
"""Run a collector immediately (not scheduled)."""
|
||||||
collector = collector_registry.get(collector_name)
|
collector = collector_registry.get(collector_name)
|
||||||
|
|||||||
207
docs/collected-data-column-removal-plan.md
Normal file
207
docs/collected-data-column-removal-plan.md
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
# collected_data 强耦合列拆除计划
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
当前 `collected_data` 同时承担了两类职责:
|
||||||
|
|
||||||
|
1. 通用采集事实表
|
||||||
|
2. 少数数据源的宽表字段承载
|
||||||
|
|
||||||
|
典型强耦合列包括:
|
||||||
|
|
||||||
|
- `country`
|
||||||
|
- `city`
|
||||||
|
- `latitude`
|
||||||
|
- `longitude`
|
||||||
|
- `value`
|
||||||
|
- `unit`
|
||||||
|
|
||||||
|
以及 API 层临时平铺出来的:
|
||||||
|
|
||||||
|
- `cores`
|
||||||
|
- `rmax`
|
||||||
|
- `rpeak`
|
||||||
|
- `power`
|
||||||
|
|
||||||
|
这些字段并不适合作为统一事实表的长期 schema。
|
||||||
|
推荐方向是:
|
||||||
|
|
||||||
|
- 表内保留通用稳定字段
|
||||||
|
- 业务差异字段全部归入 `metadata`
|
||||||
|
- API 和前端动态读取 `metadata`
|
||||||
|
|
||||||
|
## 拆除目标
|
||||||
|
|
||||||
|
最终希望 `collected_data` 只保留:
|
||||||
|
|
||||||
|
- `id`
|
||||||
|
- `snapshot_id`
|
||||||
|
- `task_id`
|
||||||
|
- `source`
|
||||||
|
- `source_id`
|
||||||
|
- `entity_key`
|
||||||
|
- `data_type`
|
||||||
|
- `name`
|
||||||
|
- `title`
|
||||||
|
- `description`
|
||||||
|
- `metadata`
|
||||||
|
- `collected_at`
|
||||||
|
- `reference_date`
|
||||||
|
- `is_valid`
|
||||||
|
- `is_current`
|
||||||
|
- `previous_record_id`
|
||||||
|
- `change_type`
|
||||||
|
- `change_summary`
|
||||||
|
- `deleted_at`
|
||||||
|
|
||||||
|
## 计划阶段
|
||||||
|
|
||||||
|
### Phase 1:读取层去依赖
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- API / 可视化 / 前端不再优先依赖宽列表字段
|
||||||
|
- 所有动态字段优先从 `metadata` 取
|
||||||
|
|
||||||
|
当前已完成:
|
||||||
|
|
||||||
|
- 新写入数据时,将 `country/city/latitude/longitude/value/unit` 自动镜像到 `metadata`
|
||||||
|
- `/api/v1/collected` 优先从 `metadata` 取动态字段
|
||||||
|
- `visualization` 接口优先从 `metadata` 取动态字段
|
||||||
|
- 国家筛选已改成只走 `metadata->>'country'`
|
||||||
|
- `CollectedData.to_dict()` 已切到 metadata-first
|
||||||
|
- 变更比较逻辑已切到 metadata-first
|
||||||
|
- 已新增历史回填脚本:
|
||||||
|
[scripts/backfill_collected_data_metadata.py](/home/ray/dev/linkong/planet/scripts/backfill_collected_data_metadata.py)
|
||||||
|
- 已新增删列脚本:
|
||||||
|
[scripts/drop_collected_data_legacy_columns.py](/home/ray/dev/linkong/planet/scripts/drop_collected_data_legacy_columns.py)
|
||||||
|
|
||||||
|
涉及文件:
|
||||||
|
|
||||||
|
- [backend/app/core/collected_data_fields.py](/home/ray/dev/linkong/planet/backend/app/core/collected_data_fields.py)
|
||||||
|
- [backend/app/services/collectors/base.py](/home/ray/dev/linkong/planet/backend/app/services/collectors/base.py)
|
||||||
|
- [backend/app/api/v1/collected_data.py](/home/ray/dev/linkong/planet/backend/app/api/v1/collected_data.py)
|
||||||
|
- [backend/app/api/v1/visualization.py](/home/ray/dev/linkong/planet/backend/app/api/v1/visualization.py)
|
||||||
|
|
||||||
|
### Phase 2:写入层去依赖
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 采集器内部不再把这些字段当作数据库一级列来理解
|
||||||
|
- 统一只写:
|
||||||
|
- 通用主字段
|
||||||
|
- `metadata`
|
||||||
|
|
||||||
|
建议动作:
|
||||||
|
|
||||||
|
1. Collector 内部仍可使用 `country/city/value` 这种临时字段作为采集过程变量
|
||||||
|
2. 进入 `BaseCollector._save_data()` 后统一归档到 `metadata`
|
||||||
|
3. `CollectedData` 模型中的强耦合列已从 ORM 移除,写入统一归档到 `metadata`
|
||||||
|
|
||||||
|
### Phase 3:数据库删列
|
||||||
|
|
||||||
|
目标:
|
||||||
|
|
||||||
|
- 从 `collected_data` 真正移除以下列:
|
||||||
|
- `country`
|
||||||
|
- `city`
|
||||||
|
- `latitude`
|
||||||
|
- `longitude`
|
||||||
|
- `value`
|
||||||
|
- `unit`
|
||||||
|
|
||||||
|
注意:
|
||||||
|
|
||||||
|
- `cores / rmax / rpeak / power` 当前本来就在 `metadata` 里,不是表列
|
||||||
|
- 这四个主要是 API 平铺字段,不需要数据库删列
|
||||||
|
|
||||||
|
## 当前阻塞点
|
||||||
|
|
||||||
|
在正式删列前,还需要确认这些地方已经完全不再直接依赖数据库列:
|
||||||
|
|
||||||
|
### 1. `CollectedData.to_dict()`
|
||||||
|
|
||||||
|
文件:
|
||||||
|
|
||||||
|
- [backend/app/models/collected_data.py](/home/ray/dev/linkong/planet/backend/app/models/collected_data.py)
|
||||||
|
|
||||||
|
状态:
|
||||||
|
|
||||||
|
- 已完成
|
||||||
|
|
||||||
|
### 2. 差异计算逻辑
|
||||||
|
|
||||||
|
文件:
|
||||||
|
|
||||||
|
- [backend/app/services/collectors/base.py](/home/ray/dev/linkong/planet/backend/app/services/collectors/base.py)
|
||||||
|
|
||||||
|
状态:
|
||||||
|
|
||||||
|
- 已完成
|
||||||
|
- 当前已改成比较归一化后的 metadata-first payload
|
||||||
|
|
||||||
|
### 3. 历史数据回填
|
||||||
|
|
||||||
|
问题:
|
||||||
|
|
||||||
|
- 老数据可能只有列值,没有对应 `metadata`
|
||||||
|
|
||||||
|
当前方案:
|
||||||
|
|
||||||
|
- 在删列前执行一次回填脚本:
|
||||||
|
- [scripts/backfill_collected_data_metadata.py](/home/ray/dev/linkong/planet/scripts/backfill_collected_data_metadata.py)
|
||||||
|
|
||||||
|
### 4. 导出格式兼容
|
||||||
|
|
||||||
|
文件:
|
||||||
|
|
||||||
|
- [backend/app/api/v1/collected_data.py](/home/ray/dev/linkong/planet/backend/app/api/v1/collected_data.py)
|
||||||
|
|
||||||
|
现状:
|
||||||
|
|
||||||
|
- CSV/JSON 导出已基本切成 metadata-first
|
||||||
|
|
||||||
|
建议:
|
||||||
|
|
||||||
|
- 删列前再回归检查一次导出字段是否一致
|
||||||
|
|
||||||
|
## 推荐执行顺序
|
||||||
|
|
||||||
|
1. 保持新数据写入时 `metadata` 完整
|
||||||
|
2. 把模型和 diff 逻辑完全切成 metadata-first
|
||||||
|
3. 写一条历史回填脚本
|
||||||
|
4. 回填后观察一轮
|
||||||
|
5. 正式执行删列迁移
|
||||||
|
|
||||||
|
## 推荐迁移 SQL
|
||||||
|
|
||||||
|
仅在确认全部读取链路已去依赖后执行:
|
||||||
|
|
||||||
|
```sql
|
||||||
|
ALTER TABLE collected_data
|
||||||
|
DROP COLUMN IF EXISTS country,
|
||||||
|
DROP COLUMN IF EXISTS city,
|
||||||
|
DROP COLUMN IF EXISTS latitude,
|
||||||
|
DROP COLUMN IF EXISTS longitude,
|
||||||
|
DROP COLUMN IF EXISTS value,
|
||||||
|
DROP COLUMN IF EXISTS unit;
|
||||||
|
```
|
||||||
|
|
||||||
|
## 风险提示
|
||||||
|
|
||||||
|
1. 地图类接口对经纬度最敏感
|
||||||
|
必须确保所有地图需要的记录,其 `metadata.latitude/longitude` 已回填完整。
|
||||||
|
|
||||||
|
2. 历史老数据如果没有回填,删列后会直接丢失这些信息。
|
||||||
|
|
||||||
|
3. 某些 collector 可能仍隐式依赖这些宽字段做差异比较,删列前必须做一次全量回归。
|
||||||
|
|
||||||
|
## 当前判断
|
||||||
|
|
||||||
|
当前项目已经完成“代码去依赖 + 历史回填 + readiness 检查”。
|
||||||
|
下一步执行顺序建议固定为:
|
||||||
|
|
||||||
|
1. 先部署当前代码版本并重启后端
|
||||||
|
2. 再做一轮功能回归
|
||||||
|
3. 最后执行:
|
||||||
|
`uv run python scripts/drop_collected_data_legacy_columns.py`
|
||||||
402
docs/collected-data-history-plan.md
Normal file
402
docs/collected-data-history-plan.md
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
# 采集数据历史快照化改造方案
|
||||||
|
|
||||||
|
## 背景
|
||||||
|
|
||||||
|
当前系统的 `collected_data` 更接近“当前结果表”:
|
||||||
|
|
||||||
|
- 同一个 `source + source_id` 会被更新覆盖
|
||||||
|
- 前端列表页默认读取这张表
|
||||||
|
- `collection_tasks` 只记录任务执行状态,不直接承载数据版本语义
|
||||||
|
|
||||||
|
这套方式适合管理后台,但不利于后续做态势感知、时间回放、趋势分析和版本对比。
|
||||||
|
如果后面需要回答下面这类问题,当前模型会比较吃力:
|
||||||
|
|
||||||
|
- 某条实体在过去 7 天如何变化
|
||||||
|
- 某次采集相比上次新增了什么、删除了什么、值变了什么
|
||||||
|
- 某个时刻地图上“当时的世界状态”是什么
|
||||||
|
- 告警是在第几次采集后触发的
|
||||||
|
|
||||||
|
因此建议把采集数据改造成“历史快照 + 当前视图”模型。
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
1. 每次触发采集都保留一份独立快照,历史可追溯。
|
||||||
|
2. 管理后台默认仍然只看“当前最新状态”,不增加使用复杂度。
|
||||||
|
3. 后续支持:
|
||||||
|
- 时间线回放
|
||||||
|
- 两次采集差异对比
|
||||||
|
- 趋势分析
|
||||||
|
- 按快照回溯告警和地图状态
|
||||||
|
4. 尽量兼容现有接口,降低改造成本。
|
||||||
|
|
||||||
|
## 结论
|
||||||
|
|
||||||
|
不建议继续用以下两种单一模式:
|
||||||
|
|
||||||
|
- 直接覆盖旧数据
|
||||||
|
问题:没有历史,无法回溯。
|
||||||
|
|
||||||
|
- 软删除旧数据再全量新增
|
||||||
|
问题:语义不清,历史和“当前无效”混在一起,后续统计复杂。
|
||||||
|
|
||||||
|
推荐方案:
|
||||||
|
|
||||||
|
- 保留历史事实表
|
||||||
|
- 维护当前视图
|
||||||
|
- 每次采集对应一个明确的快照批次
|
||||||
|
|
||||||
|
## 推荐数据模型
|
||||||
|
|
||||||
|
### 方案概览
|
||||||
|
|
||||||
|
建议拆成三层:
|
||||||
|
|
||||||
|
1. `collection_tasks`
|
||||||
|
继续作为采集任务表,表示“这次采集任务”。
|
||||||
|
|
||||||
|
2. `data_snapshots`
|
||||||
|
新增快照表,表示“某个数据源在某次任务中产出的一个快照批次”。
|
||||||
|
|
||||||
|
3. `collected_data`
|
||||||
|
从“当前结果表”升级为“历史事实表”,每一行归属于一个快照。
|
||||||
|
|
||||||
|
同时再提供一个“当前视图”:
|
||||||
|
|
||||||
|
- SQL View / 物化视图 / API 查询层封装均可
|
||||||
|
- 语义是“每个 `source + source_id` 的最新有效记录”
|
||||||
|
|
||||||
|
### 新增表:`data_snapshots`
|
||||||
|
|
||||||
|
建议字段:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 含义 |
|
||||||
|
|---|---|---|
|
||||||
|
| `id` | bigint PK | 快照主键 |
|
||||||
|
| `datasource_id` | int | 对应数据源 |
|
||||||
|
| `task_id` | int | 对应采集任务 |
|
||||||
|
| `source` | varchar(100) | 数据源名,如 `top500` |
|
||||||
|
| `snapshot_key` | varchar(100) | 可选,业务快照标识 |
|
||||||
|
| `reference_date` | timestamptz nullable | 这批数据的参考时间 |
|
||||||
|
| `started_at` | timestamptz | 快照开始时间 |
|
||||||
|
| `completed_at` | timestamptz | 快照完成时间 |
|
||||||
|
| `record_count` | int | 快照总记录数 |
|
||||||
|
| `status` | varchar(20) | `running/success/failed/partial` |
|
||||||
|
| `is_current` | bool | 当前是否是该数据源最新快照 |
|
||||||
|
| `parent_snapshot_id` | bigint nullable | 上一版快照,可用于 diff |
|
||||||
|
| `summary` | jsonb | 本次快照统计摘要 |
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- `collection_tasks` 偏“执行过程”
|
||||||
|
- `data_snapshots` 偏“数据版本”
|
||||||
|
- 一个任务通常对应一个快照,但保留分层更清晰
|
||||||
|
|
||||||
|
### 升级表:`collected_data`
|
||||||
|
|
||||||
|
建议新增字段:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 含义 |
|
||||||
|
|---|---|---|
|
||||||
|
| `snapshot_id` | bigint not null | 归属快照 |
|
||||||
|
| `task_id` | int nullable | 归属任务,便于追查 |
|
||||||
|
| `entity_key` | varchar(255) | 实体稳定键,通常可由 `source + source_id` 派生 |
|
||||||
|
| `is_current` | bool | 当前是否为该实体最新记录 |
|
||||||
|
| `previous_record_id` | bigint nullable | 上一个版本的记录 |
|
||||||
|
| `change_type` | varchar(20) | `created/updated/unchanged/deleted` |
|
||||||
|
| `change_summary` | jsonb | 字段变化摘要 |
|
||||||
|
| `deleted_at` | timestamptz nullable | 对应“本次快照中消失”的实体 |
|
||||||
|
|
||||||
|
保留现有字段:
|
||||||
|
|
||||||
|
- `source`
|
||||||
|
- `source_id`
|
||||||
|
- `data_type`
|
||||||
|
- `name`
|
||||||
|
- `title`
|
||||||
|
- `description`
|
||||||
|
- `country`
|
||||||
|
- `city`
|
||||||
|
- `latitude`
|
||||||
|
- `longitude`
|
||||||
|
- `value`
|
||||||
|
- `unit`
|
||||||
|
- `metadata`
|
||||||
|
- `collected_at`
|
||||||
|
- `reference_date`
|
||||||
|
- `is_valid`
|
||||||
|
|
||||||
|
### 当前视图
|
||||||
|
|
||||||
|
建议新增一个只读视图:
|
||||||
|
|
||||||
|
`current_collected_data`
|
||||||
|
|
||||||
|
语义:
|
||||||
|
|
||||||
|
- 对每个 `source + source_id` 只保留最新一条 `is_current = true` 且 `deleted_at is null` 的记录
|
||||||
|
|
||||||
|
这样:
|
||||||
|
|
||||||
|
- 管理后台继续像现在一样查“当前数据”
|
||||||
|
- 历史分析查 `collected_data`
|
||||||
|
|
||||||
|
## 写入策略
|
||||||
|
|
||||||
|
### 触发按钮语义
|
||||||
|
|
||||||
|
“触发”不再理解为“覆盖旧表”,而是:
|
||||||
|
|
||||||
|
- 启动一次新的采集任务
|
||||||
|
- 生成一个新的快照
|
||||||
|
- 将本次结果写入历史事实表
|
||||||
|
- 再更新当前视图标记
|
||||||
|
|
||||||
|
### 写入流程
|
||||||
|
|
||||||
|
1. 创建 `collection_tasks` 记录,状态 `running`
|
||||||
|
2. 创建 `data_snapshots` 记录,状态 `running`
|
||||||
|
3. 采集器拉取原始数据并标准化
|
||||||
|
4. 为每条记录生成 `entity_key`
|
||||||
|
- 推荐:`{source}:{source_id}`
|
||||||
|
5. 将本次记录批量写入 `collected_data`
|
||||||
|
6. 与上一个快照做比对,计算:
|
||||||
|
- 新增
|
||||||
|
- 更新
|
||||||
|
- 未变
|
||||||
|
- 删除
|
||||||
|
7. 更新本批记录的:
|
||||||
|
- `change_type`
|
||||||
|
- `previous_record_id`
|
||||||
|
- `is_current`
|
||||||
|
8. 将上一批同实体记录的 `is_current` 置为 `false`
|
||||||
|
9. 将本次快照未出现但上一版存在的实体标记为 `deleted`
|
||||||
|
10. 更新 `data_snapshots.status = success`
|
||||||
|
11. 更新 `collection_tasks.status = success`
|
||||||
|
|
||||||
|
### 删除语义
|
||||||
|
|
||||||
|
这里不建议真的删记录。
|
||||||
|
建议采用“逻辑消失”模型:
|
||||||
|
|
||||||
|
- 历史行永远保留
|
||||||
|
- 如果某实体在新快照里消失:
|
||||||
|
- 上一条历史记录补一条“删除状态记录”或标记 `change_type = deleted`
|
||||||
|
- 同时该实体不再出现在当前视图
|
||||||
|
|
||||||
|
这样最适合态势感知。
|
||||||
|
|
||||||
|
## API 改造建议
|
||||||
|
|
||||||
|
### 保持现有接口默认行为
|
||||||
|
|
||||||
|
现有接口:
|
||||||
|
|
||||||
|
- `GET /api/v1/collected`
|
||||||
|
- `GET /api/v1/collected/{id}`
|
||||||
|
- `GET /api/v1/collected/summary`
|
||||||
|
|
||||||
|
建议默认仍返回“当前视图”,避免前端全面重写。
|
||||||
|
|
||||||
|
### 新增历史查询能力
|
||||||
|
|
||||||
|
建议新增参数或新接口:
|
||||||
|
|
||||||
|
#### 1. 当前/历史切换
|
||||||
|
|
||||||
|
`GET /api/v1/collected?mode=current|history`
|
||||||
|
|
||||||
|
- `current`:默认,查当前视图
|
||||||
|
- `history`:查历史事实表
|
||||||
|
|
||||||
|
#### 2. 按快照查询
|
||||||
|
|
||||||
|
`GET /api/v1/collected?snapshot_id=123`
|
||||||
|
|
||||||
|
#### 3. 快照列表
|
||||||
|
|
||||||
|
`GET /api/v1/snapshots`
|
||||||
|
|
||||||
|
支持筛选:
|
||||||
|
|
||||||
|
- `datasource_id`
|
||||||
|
- `source`
|
||||||
|
- `status`
|
||||||
|
- `date_from/date_to`
|
||||||
|
|
||||||
|
#### 4. 快照详情
|
||||||
|
|
||||||
|
`GET /api/v1/snapshots/{id}`
|
||||||
|
|
||||||
|
返回:
|
||||||
|
|
||||||
|
- 快照基础信息
|
||||||
|
- 统计摘要
|
||||||
|
- 与上一版的 diff 摘要
|
||||||
|
|
||||||
|
#### 5. 快照 diff
|
||||||
|
|
||||||
|
`GET /api/v1/snapshots/{id}/diff?base_snapshot_id=122`
|
||||||
|
|
||||||
|
返回:
|
||||||
|
|
||||||
|
- `created`
|
||||||
|
- `updated`
|
||||||
|
- `deleted`
|
||||||
|
- `unchanged`
|
||||||
|
|
||||||
|
## 前端改造建议
|
||||||
|
|
||||||
|
### 1. 数据列表页
|
||||||
|
|
||||||
|
默认仍看当前数据,不改用户使用习惯。
|
||||||
|
|
||||||
|
建议新增:
|
||||||
|
|
||||||
|
- “视图模式”
|
||||||
|
- 当前数据
|
||||||
|
- 历史数据
|
||||||
|
- “快照时间”筛选
|
||||||
|
- “只看变化项”筛选
|
||||||
|
|
||||||
|
### 2. 数据详情页
|
||||||
|
|
||||||
|
详情页建议展示:
|
||||||
|
|
||||||
|
- 当前记录基础信息
|
||||||
|
- 元数据动态字段
|
||||||
|
- 所属快照
|
||||||
|
- 上一版本对比入口
|
||||||
|
- 历史版本时间线
|
||||||
|
|
||||||
|
### 3. 数据源管理页
|
||||||
|
|
||||||
|
“触发”按钮文案建议改成更准确的:
|
||||||
|
|
||||||
|
- `立即采集`
|
||||||
|
|
||||||
|
并在详情里补:
|
||||||
|
|
||||||
|
- 最近一次快照时间
|
||||||
|
- 最近一次快照记录数
|
||||||
|
- 最近一次变化数
|
||||||
|
|
||||||
|
## 迁移方案
|
||||||
|
|
||||||
|
### Phase 1:兼容式落地
|
||||||
|
|
||||||
|
目标:先保留当前页面可用。
|
||||||
|
|
||||||
|
改动:
|
||||||
|
|
||||||
|
1. 新增 `data_snapshots`
|
||||||
|
2. 给 `collected_data` 增加:
|
||||||
|
- `snapshot_id`
|
||||||
|
- `task_id`
|
||||||
|
- `entity_key`
|
||||||
|
- `is_current`
|
||||||
|
- `previous_record_id`
|
||||||
|
- `change_type`
|
||||||
|
- `change_summary`
|
||||||
|
- `deleted_at`
|
||||||
|
3. 现有数据全部补成一个“初始化快照”
|
||||||
|
4. 现有 `/collected` 默认改查当前视图
|
||||||
|
|
||||||
|
优点:
|
||||||
|
|
||||||
|
- 前端几乎无感
|
||||||
|
- 风险最小
|
||||||
|
|
||||||
|
### Phase 2:启用差异计算
|
||||||
|
|
||||||
|
目标:采集后可知道本次改了什么。
|
||||||
|
|
||||||
|
改动:
|
||||||
|
|
||||||
|
1. 写入时做新旧快照比对
|
||||||
|
2. 写 `change_type`
|
||||||
|
3. 生成快照摘要
|
||||||
|
|
||||||
|
### Phase 3:前端态势感知能力
|
||||||
|
|
||||||
|
目标:支持历史回放和趋势分析。
|
||||||
|
|
||||||
|
改动:
|
||||||
|
|
||||||
|
1. 快照时间线
|
||||||
|
2. 版本 diff 页面
|
||||||
|
3. 地图时间回放
|
||||||
|
4. 告警和快照关联
|
||||||
|
|
||||||
|
## 唯一性与索引建议
|
||||||
|
|
||||||
|
### 建议保留的业务唯一性
|
||||||
|
|
||||||
|
在“同一个快照内部”,建议唯一:
|
||||||
|
|
||||||
|
- `(snapshot_id, source, source_id)`
|
||||||
|
|
||||||
|
不要在整张历史表上强加:
|
||||||
|
|
||||||
|
- `(source, source_id)` 唯一
|
||||||
|
|
||||||
|
因为历史表本来就应该允许同一实体跨快照存在多条版本。
|
||||||
|
|
||||||
|
### 建议索引
|
||||||
|
|
||||||
|
- `idx_collected_data_snapshot_id`
|
||||||
|
- `idx_collected_data_source_source_id`
|
||||||
|
- `idx_collected_data_entity_key`
|
||||||
|
- `idx_collected_data_is_current`
|
||||||
|
- `idx_collected_data_reference_date`
|
||||||
|
- `idx_snapshots_source_completed_at`
|
||||||
|
|
||||||
|
## 风险点
|
||||||
|
|
||||||
|
1. 存储量会明显增加
|
||||||
|
- 需要评估保留周期
|
||||||
|
- 可以考虑冷热分层
|
||||||
|
|
||||||
|
2. 写入复杂度上升
|
||||||
|
- 需要批量 upsert / diff 逻辑
|
||||||
|
|
||||||
|
3. 当前接口语义会从“表”变成“视图”
|
||||||
|
- 文档必须同步
|
||||||
|
|
||||||
|
4. 某些采集器缺稳定 `source_id`
|
||||||
|
- 需要补齐实体稳定键策略
|
||||||
|
|
||||||
|
## 对当前项目的具体建议
|
||||||
|
|
||||||
|
结合当前代码,推荐这样落地:
|
||||||
|
|
||||||
|
### 短期
|
||||||
|
|
||||||
|
1. 先设计并落表:
|
||||||
|
- `data_snapshots`
|
||||||
|
- `collected_data` 新字段
|
||||||
|
2. 采集完成后每次新增快照
|
||||||
|
3. `/api/v1/collected` 默认查 `is_current = true`
|
||||||
|
|
||||||
|
### 中期
|
||||||
|
|
||||||
|
1. 在 `BaseCollector._save_data()` 中改成:
|
||||||
|
- 生成快照
|
||||||
|
- 批量写历史
|
||||||
|
- 标记当前
|
||||||
|
2. 将 `CollectionTask.id` 关联到 `snapshot.task_id`
|
||||||
|
|
||||||
|
### 长期
|
||||||
|
|
||||||
|
1. 地图接口支持按 `snapshot_id` 查询
|
||||||
|
2. 仪表盘支持“最近一次快照变化量”
|
||||||
|
3. 告警支持绑定到快照版本
|
||||||
|
|
||||||
|
## 最终建议
|
||||||
|
|
||||||
|
最终建议采用:
|
||||||
|
|
||||||
|
- 历史事实表:保存每次采集结果
|
||||||
|
- 当前视图:服务管理后台默认查询
|
||||||
|
- 快照表:承载版本批次和 diff 语义
|
||||||
|
|
||||||
|
这样既能保留历史,又不会把当前页面全部推翻重做,是最适合后续做态势感知的一条路径。
|
||||||
@@ -45,3 +45,4 @@
|
|||||||
- 配置页可以查看并修改所有内置采集器的启停与采集频率
|
- 配置页可以查看并修改所有内置采集器的启停与采集频率
|
||||||
- 调整采集频率后,调度器任务随之更新
|
- 调整采集频率后,调度器任务随之更新
|
||||||
- `/settings` 页面可从主导航进入并正常工作
|
- `/settings` 页面可从主导航进入并正常工作
|
||||||
|
|
||||||
|
|||||||
@@ -231,6 +231,10 @@ body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.data-source-tabs .ant-tabs-tabpane-hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.data-source-custom-tab {
|
.data-source-custom-tab {
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
@@ -340,6 +344,42 @@ body {
|
|||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.table-scroll-region .ant-table-thead > tr > th,
|
||||||
|
.table-scroll-region .ant-table-tbody > tr > td {
|
||||||
|
padding: 10px 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-scroll-region .ant-table-body,
|
||||||
|
.table-scroll-region .ant-table-content {
|
||||||
|
scrollbar-width: thin;
|
||||||
|
scrollbar-color: rgba(148, 163, 184, 0.88) transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-scroll-region .ant-table-body::-webkit-scrollbar,
|
||||||
|
.table-scroll-region .ant-table-content::-webkit-scrollbar {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-scroll-region .ant-table-body::-webkit-scrollbar-thumb,
|
||||||
|
.table-scroll-region .ant-table-content::-webkit-scrollbar-thumb {
|
||||||
|
background: rgba(148, 163, 184, 0.82);
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-scroll-region .ant-table-body::-webkit-scrollbar-thumb:hover,
|
||||||
|
.table-scroll-region .ant-table-content::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: rgba(100, 116, 139, 0.9);
|
||||||
|
background-clip: padding-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-scroll-region .ant-table-body::-webkit-scrollbar-track,
|
||||||
|
.table-scroll-region .ant-table-content::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
.settings-shell,
|
.settings-shell,
|
||||||
.settings-tabs-shell,
|
.settings-tabs-shell,
|
||||||
.settings-tabs,
|
.settings-tabs,
|
||||||
@@ -377,7 +417,7 @@ body {
|
|||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-tab-panel {
|
.settings-pane {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -427,9 +467,22 @@ body {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-table-scroll-region {
|
.settings-pane .data-source-table-region .ant-table-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-pane .data-source-table-region .ant-table-header {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-pane .data-source-table-region .ant-table-body {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
overflow: hidden;
|
min-height: 0;
|
||||||
|
height: 0 !important;
|
||||||
|
max-height: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -490,6 +543,10 @@ body {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.data-list-summary-card-inner {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.data-list-right-column {
|
.data-list-right-column {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -499,7 +556,9 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.data-list-summary-treemap {
|
.data-list-summary-treemap {
|
||||||
min-height: 100%;
|
--data-list-treemap-tile-padding: 12px;
|
||||||
|
--data-list-treemap-label-size: 12px;
|
||||||
|
--data-list-treemap-value-size: 16px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
grid-auto-rows: minmax(56px, 1fr);
|
grid-auto-rows: minmax(56px, 1fr);
|
||||||
@@ -512,9 +571,9 @@ body {
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-between;
|
justify-content: flex-start;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
padding: 12px;
|
padding: var(--data-list-treemap-tile-padding);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.55);
|
border: 1px solid rgba(255, 255, 255, 0.55);
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
@@ -552,29 +611,36 @@ body {
|
|||||||
.data-list-treemap-head {
|
.data-list-treemap-head {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-list-treemap-label {
|
.data-list-treemap-label {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
font-size: clamp(11px, 0.75vw, 13px);
|
font-size: var(--data-list-treemap-label-size);
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
color: rgba(15, 23, 42, 0.78);
|
color: rgba(15, 23, 42, 0.78);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-list-treemap-body {
|
.data-list-treemap-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 4px;
|
gap: 2px;
|
||||||
|
margin-top: auto;
|
||||||
|
min-height: 0;
|
||||||
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-list-summary-tile-icon {
|
.data-list-summary-tile-icon {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 24px;
|
width: 22px;
|
||||||
height: 24px;
|
height: 22px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background: rgba(255, 255, 255, 0.55);
|
background: rgba(255, 255, 255, 0.55);
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
@@ -582,9 +648,12 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.data-list-summary-tile-value {
|
.data-list-summary-tile-value {
|
||||||
font-size: clamp(12px, 1vw, 16px);
|
font-size: var(--data-list-treemap-value-size);
|
||||||
line-height: 1.1;
|
line-height: 1.1;
|
||||||
color: #0f172a;
|
color: #0f172a;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-list-treemap-meta {
|
.data-list-treemap-meta {
|
||||||
@@ -611,7 +680,7 @@ body {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
align-items: center;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
.data-list-filter-grid--balanced > * {
|
.data-list-filter-grid--balanced > * {
|
||||||
@@ -687,6 +756,46 @@ body {
|
|||||||
margin: 12px 0 0;
|
margin: 12px 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.data-list-name-link {
|
||||||
|
max-width: 100%;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding-inline: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-name-marquee {
|
||||||
|
display: block;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-name-marquee--overflow {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-name-marquee__text {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
|
white-space: nowrap;
|
||||||
|
transform: translateX(0);
|
||||||
|
will-change: transform;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-name-link:hover .data-list-name-marquee--overflow .data-list-name-marquee__text {
|
||||||
|
animation: data-list-name-marquee 8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes data-list-name-marquee {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.data-list-resize-handle {
|
.data-list-resize-handle {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -807,3 +916,172 @@ body {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.data-list-detail-modal {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-detail-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-detail-section__title {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-detail-hero {
|
||||||
|
padding: 14px 16px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #f7f8fa;
|
||||||
|
border: 1px solid #eef1f5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-detail-hero__label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-detail-hero__title.ant-typography {
|
||||||
|
margin: 0;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-detail-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-detail-cell {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 1px solid #eef1f5;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-detail-cell--block {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-detail-cell__label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-detail-cell__value {
|
||||||
|
color: #111827;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-detail-code {
|
||||||
|
margin: 0;
|
||||||
|
padding: 12px;
|
||||||
|
max-height: 240px;
|
||||||
|
overflow: auto;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #111827;
|
||||||
|
color: #e5eef9;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-detail-code--raw {
|
||||||
|
max-height: 320px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-tag-cell {
|
||||||
|
min-width: 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-tag-cell .ant-tag {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
|
white-space: normal;
|
||||||
|
overflow-wrap: anywhere;
|
||||||
|
word-break: break-word;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-filter-select {
|
||||||
|
max-width: 220px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-filter-select .ant-select-selector {
|
||||||
|
height: auto !important;
|
||||||
|
min-height: 32px;
|
||||||
|
max-height: 72px;
|
||||||
|
align-items: flex-start !important;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-filter-select .ant-select-selection-overflow {
|
||||||
|
flex-wrap: wrap !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-filter-select .ant-select-selection-overflow-item {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.data-list-filter-select .ant-select-selection-item {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-page__header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-page__actions {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-status-tag {
|
||||||
|
margin-inline-end: 0 !important;
|
||||||
|
padding-inline: 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
line-height: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-refresh-button.ant-btn {
|
||||||
|
height: 26px;
|
||||||
|
padding-inline: 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border-color: #d9d9d9;
|
||||||
|
background: #ffffff;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboard-refresh-button.ant-btn:hover,
|
||||||
|
.dashboard-refresh-button.ant-btn:focus {
|
||||||
|
border-color: #bfbfbf;
|
||||||
|
background: #ffffff;
|
||||||
|
color: rgba(0, 0, 0, 0.88);
|
||||||
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<BrowserRouter>
|
<BrowserRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
||||||
<App />
|
<App />
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
|
|||||||
@@ -122,19 +122,19 @@ function Dashboard() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<AppLayout>
|
<AppLayout>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
<div className="dashboard-page">
|
||||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12, flexWrap: 'wrap' }}>
|
<div className="dashboard-page__header">
|
||||||
<div>
|
<div>
|
||||||
<Title level={4} style={{ margin: 0 }}>仪表盘</Title>
|
<Title level={4} style={{ margin: 0 }}>仪表盘</Title>
|
||||||
<Text type="secondary">系统总览与实时态势</Text>
|
<Text type="secondary">系统总览与实时态势</Text>
|
||||||
</div>
|
</div>
|
||||||
<Space wrap>
|
<Space wrap className="dashboard-page__actions">
|
||||||
{wsConnected ? (
|
{wsConnected ? (
|
||||||
<Tag icon={<WifiOutlined />} color="success">实时连接</Tag>
|
<Tag className="dashboard-status-tag" icon={<WifiOutlined />} color="success">实时连接</Tag>
|
||||||
) : (
|
) : (
|
||||||
<Tag icon={<DisconnectOutlined />} color="default">离线</Tag>
|
<Tag className="dashboard-status-tag" icon={<DisconnectOutlined />} color="default">离线</Tag>
|
||||||
)}
|
)}
|
||||||
<Button type="default" icon={<ReloadOutlined />} onClick={handleRetry}>刷新</Button>
|
<Button className="dashboard-refresh-button" icon={<ReloadOutlined />} onClick={handleRetry}>刷新</Button>
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -188,7 +188,7 @@ function Dashboard() {
|
|||||||
{stats?.last_updated && (
|
{stats?.last_updated && (
|
||||||
<div style={{ textAlign: 'center', color: '#8c8c8c' }}>
|
<div style={{ textAlign: 'center', color: '#8c8c8c' }}>
|
||||||
最后更新: {new Date(stats.last_updated).toLocaleString('zh-CN')}
|
最后更新: {new Date(stats.last_updated).toLocaleString('zh-CN')}
|
||||||
{wsConnected && <Tag color="green" style={{ marginLeft: 8 }}>实时同步中</Tag>}
|
{wsConnected && <Tag className="dashboard-status-tag" color="green" style={{ marginLeft: 8 }}>实时同步中</Tag>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useLayoutEffect, useMemo, useRef, useState, type CSSProperties } from 'react'
|
||||||
import {
|
import {
|
||||||
Table, Tag, Space, Card, Select, Input, Button,
|
Table, Tag, Space, Card, Select, Input, Button,
|
||||||
Modal, Descriptions, Spin, Empty, Tooltip, Typography, Grid
|
Modal, Spin, Empty, Tooltip, Typography, Grid
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import type { ColumnsType } from 'antd/es/table'
|
import type { ColumnsType } from 'antd/es/table'
|
||||||
|
import type { CustomTagProps } from 'rc-select/lib/BaseSelect'
|
||||||
import {
|
import {
|
||||||
DatabaseOutlined, GlobalOutlined, CloudServerOutlined,
|
DatabaseOutlined, GlobalOutlined, CloudServerOutlined,
|
||||||
AppstoreOutlined, EyeOutlined, SearchOutlined, FilterOutlined, ReloadOutlined
|
AppstoreOutlined, EyeOutlined, SearchOutlined, FilterOutlined, ReloadOutlined
|
||||||
@@ -28,6 +29,10 @@ interface CollectedData {
|
|||||||
longitude: string | null
|
longitude: string | null
|
||||||
value: string | null
|
value: string | null
|
||||||
unit: string | null
|
unit: string | null
|
||||||
|
cores: string | null
|
||||||
|
rmax: string | null
|
||||||
|
rpeak: string | null
|
||||||
|
power: string | null
|
||||||
metadata: Record<string, any> | null
|
metadata: Record<string, any> | null
|
||||||
collected_at: string
|
collected_at: string
|
||||||
reference_date: string | null
|
reference_date: string | null
|
||||||
@@ -40,6 +45,183 @@ interface Summary {
|
|||||||
source_totals: Array<{ source: string; count: number }>
|
source_totals: Array<{ source: string; count: number }>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DETAIL_FIELD_LABELS: Record<string, string> = {
|
||||||
|
id: 'ID',
|
||||||
|
source: '数据源',
|
||||||
|
source_id: '原始ID',
|
||||||
|
data_type: '数据类型',
|
||||||
|
name: '名称',
|
||||||
|
title: '标题',
|
||||||
|
description: '描述',
|
||||||
|
country: '国家',
|
||||||
|
city: '城市',
|
||||||
|
latitude: '纬度',
|
||||||
|
longitude: '经度',
|
||||||
|
value: '数值',
|
||||||
|
unit: '单位',
|
||||||
|
collected_at: '采集时间',
|
||||||
|
reference_date: '参考日期',
|
||||||
|
is_valid: '有效状态',
|
||||||
|
rank: '排名',
|
||||||
|
cores: '核心数量',
|
||||||
|
rmax: '实际最大算力',
|
||||||
|
rpeak: '理论算力',
|
||||||
|
power: '功耗',
|
||||||
|
manufacturer: '厂商',
|
||||||
|
site: '站点',
|
||||||
|
processor: '处理器',
|
||||||
|
interconnect: '互连',
|
||||||
|
installation_year: '安装年份',
|
||||||
|
nmax: 'Nmax',
|
||||||
|
hpcg: 'HPCG',
|
||||||
|
power_measurement_level: '功耗测量等级',
|
||||||
|
operating_system: '操作系统',
|
||||||
|
compiler: '编译器',
|
||||||
|
math_library: '数学库',
|
||||||
|
mpi: 'MPI',
|
||||||
|
raw_country: '原始国家值',
|
||||||
|
country_validation: '国家校验',
|
||||||
|
}
|
||||||
|
|
||||||
|
const DETAIL_BASE_FIELDS = [
|
||||||
|
'source',
|
||||||
|
'data_type',
|
||||||
|
'source_id',
|
||||||
|
'country',
|
||||||
|
'city',
|
||||||
|
'collected_at',
|
||||||
|
'reference_date',
|
||||||
|
]
|
||||||
|
|
||||||
|
function formatFieldLabel(key: string) {
|
||||||
|
if (DETAIL_FIELD_LABELS[key]) {
|
||||||
|
return DETAIL_FIELD_LABELS[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
return key
|
||||||
|
.split('_')
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
||||||
|
.join(' ')
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDetailValue(key: string, value: unknown) {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === 'collected_at' || key === 'reference_date') {
|
||||||
|
const date = new Date(String(value))
|
||||||
|
return Number.isNaN(date.getTime())
|
||||||
|
? String(value)
|
||||||
|
: key === 'reference_date'
|
||||||
|
? date.toLocaleDateString('zh-CN')
|
||||||
|
: date.toLocaleString('zh-CN')
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
return value ? '是' : '否'
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'object') {
|
||||||
|
return JSON.stringify(value, null, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NameMarquee({ text }: { text: string }) {
|
||||||
|
const containerRef = useRef<HTMLSpanElement | null>(null)
|
||||||
|
const textRef = useRef<HTMLSpanElement | null>(null)
|
||||||
|
const [overflowing, setOverflowing] = useState(false)
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const updateOverflow = () => {
|
||||||
|
const container = containerRef.current
|
||||||
|
const content = textRef.current
|
||||||
|
if (!container || !content) return
|
||||||
|
setOverflowing(content.scrollWidth > container.clientWidth + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateOverflow()
|
||||||
|
|
||||||
|
if (typeof ResizeObserver === 'undefined') {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(updateOverflow)
|
||||||
|
if (containerRef.current) observer.observe(containerRef.current)
|
||||||
|
if (textRef.current) observer.observe(textRef.current)
|
||||||
|
|
||||||
|
return () => observer.disconnect()
|
||||||
|
}, [text])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
ref={containerRef}
|
||||||
|
className={`data-list-name-marquee${overflowing ? ' data-list-name-marquee--overflow' : ''}`}
|
||||||
|
>
|
||||||
|
<span ref={textRef} className="data-list-name-marquee__text">
|
||||||
|
{text}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function estimateTreemapRows(
|
||||||
|
items: Array<{ colSpan: number; rowSpan: number }>,
|
||||||
|
columns: number
|
||||||
|
): number {
|
||||||
|
const occupancy: boolean[][] = []
|
||||||
|
|
||||||
|
const ensureRow = (rowIndex: number) => {
|
||||||
|
while (occupancy.length <= rowIndex) {
|
||||||
|
occupancy.push(Array(columns).fill(false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
let placed = false
|
||||||
|
let rowIndex = 0
|
||||||
|
|
||||||
|
while (!placed) {
|
||||||
|
ensureRow(rowIndex)
|
||||||
|
|
||||||
|
for (let columnIndex = 0; columnIndex <= columns - item.colSpan; columnIndex += 1) {
|
||||||
|
let canPlace = true
|
||||||
|
|
||||||
|
for (let rowOffset = 0; rowOffset < item.rowSpan; rowOffset += 1) {
|
||||||
|
ensureRow(rowIndex + rowOffset)
|
||||||
|
|
||||||
|
for (let columnOffset = 0; columnOffset < item.colSpan; columnOffset += 1) {
|
||||||
|
if (occupancy[rowIndex + rowOffset][columnIndex + columnOffset]) {
|
||||||
|
canPlace = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canPlace) break
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!canPlace) continue
|
||||||
|
|
||||||
|
for (let rowOffset = 0; rowOffset < item.rowSpan; rowOffset += 1) {
|
||||||
|
for (let columnOffset = 0; columnOffset < item.colSpan; columnOffset += 1) {
|
||||||
|
occupancy[rowIndex + rowOffset][columnIndex + columnOffset] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
placed = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
rowIndex += 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(occupancy.length, 1)
|
||||||
|
}
|
||||||
|
|
||||||
function DataList() {
|
function DataList() {
|
||||||
const screens = useBreakpoint()
|
const screens = useBreakpoint()
|
||||||
const isCompact = !screens.lg
|
const isCompact = !screens.lg
|
||||||
@@ -48,6 +230,7 @@ function DataList() {
|
|||||||
const mainAreaRef = useRef<HTMLDivElement | null>(null)
|
const mainAreaRef = useRef<HTMLDivElement | null>(null)
|
||||||
const rightColumnRef = useRef<HTMLDivElement | null>(null)
|
const rightColumnRef = useRef<HTMLDivElement | null>(null)
|
||||||
const tableHeaderRef = useRef<HTMLDivElement | null>(null)
|
const tableHeaderRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const summaryBodyRef = useRef<HTMLDivElement | null>(null)
|
||||||
const hasCustomLeftWidthRef = useRef(false)
|
const hasCustomLeftWidthRef = useRef(false)
|
||||||
|
|
||||||
const [mainAreaWidth, setMainAreaWidth] = useState(0)
|
const [mainAreaWidth, setMainAreaWidth] = useState(0)
|
||||||
@@ -55,6 +238,7 @@ function DataList() {
|
|||||||
const [rightColumnHeight, setRightColumnHeight] = useState(0)
|
const [rightColumnHeight, setRightColumnHeight] = useState(0)
|
||||||
const [tableHeaderHeight, setTableHeaderHeight] = useState(0)
|
const [tableHeaderHeight, setTableHeaderHeight] = useState(0)
|
||||||
const [leftPanelWidth, setLeftPanelWidth] = useState(360)
|
const [leftPanelWidth, setLeftPanelWidth] = useState(360)
|
||||||
|
const [summaryBodyHeight, setSummaryBodyHeight] = useState(0)
|
||||||
|
|
||||||
const [data, setData] = useState<CollectedData[]>([])
|
const [data, setData] = useState<CollectedData[]>([])
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
@@ -62,13 +246,11 @@ function DataList() {
|
|||||||
const [total, setTotal] = useState(0)
|
const [total, setTotal] = useState(0)
|
||||||
const [page, setPage] = useState(1)
|
const [page, setPage] = useState(1)
|
||||||
const [pageSize, setPageSize] = useState(20)
|
const [pageSize, setPageSize] = useState(20)
|
||||||
const [sourceFilter, setSourceFilter] = useState<string | undefined>()
|
const [sourceFilter, setSourceFilter] = useState<string[]>([])
|
||||||
const [typeFilter, setTypeFilter] = useState<string | undefined>()
|
const [typeFilter, setTypeFilter] = useState<string[]>([])
|
||||||
const [countryFilter, setCountryFilter] = useState<string | undefined>()
|
|
||||||
const [searchText, setSearchText] = useState('')
|
const [searchText, setSearchText] = useState('')
|
||||||
const [sources, setSources] = useState<string[]>([])
|
const [sources, setSources] = useState<string[]>([])
|
||||||
const [types, setTypes] = useState<string[]>([])
|
const [types, setTypes] = useState<string[]>([])
|
||||||
const [countries, setCountries] = useState<string[]>([])
|
|
||||||
const [detailVisible, setDetailVisible] = useState(false)
|
const [detailVisible, setDetailVisible] = useState(false)
|
||||||
const [detailData, setDetailData] = useState<CollectedData | null>(null)
|
const [detailData, setDetailData] = useState<CollectedData | null>(null)
|
||||||
const [detailLoading, setDetailLoading] = useState(false)
|
const [detailLoading, setDetailLoading] = useState(false)
|
||||||
@@ -79,6 +261,7 @@ function DataList() {
|
|||||||
setMainAreaHeight(mainAreaRef.current?.offsetHeight || 0)
|
setMainAreaHeight(mainAreaRef.current?.offsetHeight || 0)
|
||||||
setRightColumnHeight(rightColumnRef.current?.offsetHeight || 0)
|
setRightColumnHeight(rightColumnRef.current?.offsetHeight || 0)
|
||||||
setTableHeaderHeight(tableHeaderRef.current?.offsetHeight || 0)
|
setTableHeaderHeight(tableHeaderRef.current?.offsetHeight || 0)
|
||||||
|
setSummaryBodyHeight(summaryBodyRef.current?.offsetHeight || 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLayout()
|
updateLayout()
|
||||||
@@ -93,6 +276,7 @@ function DataList() {
|
|||||||
if (mainAreaRef.current) observer.observe(mainAreaRef.current)
|
if (mainAreaRef.current) observer.observe(mainAreaRef.current)
|
||||||
if (rightColumnRef.current) observer.observe(rightColumnRef.current)
|
if (rightColumnRef.current) observer.observe(rightColumnRef.current)
|
||||||
if (tableHeaderRef.current) observer.observe(tableHeaderRef.current)
|
if (tableHeaderRef.current) observer.observe(tableHeaderRef.current)
|
||||||
|
if (summaryBodyRef.current) observer.observe(summaryBodyRef.current)
|
||||||
|
|
||||||
return () => observer.disconnect()
|
return () => observer.disconnect()
|
||||||
}, [isCompact])
|
}, [isCompact])
|
||||||
@@ -147,9 +331,8 @@ function DataList() {
|
|||||||
page: page.toString(),
|
page: page.toString(),
|
||||||
page_size: pageSize.toString(),
|
page_size: pageSize.toString(),
|
||||||
})
|
})
|
||||||
if (sourceFilter) params.append('source', sourceFilter)
|
if (sourceFilter.length > 0) params.append('source', sourceFilter.join(','))
|
||||||
if (typeFilter) params.append('data_type', typeFilter)
|
if (typeFilter.length > 0) params.append('data_type', typeFilter.join(','))
|
||||||
if (countryFilter) params.append('country', countryFilter)
|
|
||||||
if (searchText) params.append('search', searchText)
|
if (searchText) params.append('search', searchText)
|
||||||
|
|
||||||
const res = await axios.get(`/api/v1/collected?${params}`)
|
const res = await axios.get(`/api/v1/collected?${params}`)
|
||||||
@@ -173,14 +356,12 @@ function DataList() {
|
|||||||
|
|
||||||
const fetchFilters = async () => {
|
const fetchFilters = async () => {
|
||||||
try {
|
try {
|
||||||
const [sourcesRes, typesRes, countriesRes] = await Promise.all([
|
const [sourcesRes, typesRes] = await Promise.all([
|
||||||
axios.get('/api/v1/collected/sources'),
|
axios.get('/api/v1/collected/sources'),
|
||||||
axios.get('/api/v1/collected/types'),
|
axios.get('/api/v1/collected/types'),
|
||||||
axios.get('/api/v1/collected/countries'),
|
|
||||||
])
|
])
|
||||||
setSources(sourcesRes.data.sources || [])
|
setSources(sourcesRes.data.sources || [])
|
||||||
setTypes(typesRes.data.data_types || [])
|
setTypes(typesRes.data.data_types || [])
|
||||||
setCountries(countriesRes.data.countries || [])
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch filters:', error)
|
console.error('Failed to fetch filters:', error)
|
||||||
}
|
}
|
||||||
@@ -193,7 +374,7 @@ function DataList() {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData()
|
fetchData()
|
||||||
}, [page, pageSize, sourceFilter, typeFilter, countryFilter])
|
}, [page, pageSize, sourceFilter, typeFilter])
|
||||||
|
|
||||||
const handleSearch = () => {
|
const handleSearch = () => {
|
||||||
setPage(1)
|
setPage(1)
|
||||||
@@ -201,9 +382,8 @@ function DataList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleReset = () => {
|
const handleReset = () => {
|
||||||
setSourceFilter(undefined)
|
setSourceFilter([])
|
||||||
setTypeFilter(undefined)
|
setTypeFilter([])
|
||||||
setCountryFilter(undefined)
|
|
||||||
setSearchText('')
|
setSearchText('')
|
||||||
setPage(1)
|
setPage(1)
|
||||||
setTimeout(fetchData, 0)
|
setTimeout(fetchData, 0)
|
||||||
@@ -234,6 +414,47 @@ function DataList() {
|
|||||||
return iconMap[source] || <DatabaseOutlined />
|
return iconMap[source] || <DatabaseOutlined />
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getSourceTagColor = (source: string) => {
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
top500: 'geekblue',
|
||||||
|
huggingface_models: 'purple',
|
||||||
|
huggingface_datasets: 'cyan',
|
||||||
|
huggingface_spaces: 'magenta',
|
||||||
|
telegeography_cables: 'green',
|
||||||
|
epoch_ai_gpu: 'volcano',
|
||||||
|
}
|
||||||
|
return colorMap[source] || 'blue'
|
||||||
|
}
|
||||||
|
|
||||||
|
const getDataTypeTagColor = (dataType: string) => {
|
||||||
|
const colorMap: Record<string, string> = {
|
||||||
|
supercomputer: 'geekblue',
|
||||||
|
model: 'purple',
|
||||||
|
dataset: 'cyan',
|
||||||
|
space: 'magenta',
|
||||||
|
submarine_cable: 'green',
|
||||||
|
cable_landing_point: 'lime',
|
||||||
|
cable_landing_relation: 'gold',
|
||||||
|
gpu_cluster: 'volcano',
|
||||||
|
generic: 'default',
|
||||||
|
}
|
||||||
|
return colorMap[dataType] || 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderFilterTag = (tagProps: CustomTagProps, getColor: (value: string) => string) => {
|
||||||
|
const { label, value, closable, onClose } = tagProps
|
||||||
|
return (
|
||||||
|
<Tag
|
||||||
|
color={getColor(String(value))}
|
||||||
|
closable={closable}
|
||||||
|
onClose={onClose}
|
||||||
|
style={{ marginInlineEnd: 4 }}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Tag>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
const getTypeColor = (type: string) => {
|
const getTypeColor = (type: string) => {
|
||||||
const colors: Record<string, string> = {
|
const colors: Record<string, string> = {
|
||||||
supercomputer: 'red',
|
supercomputer: 'red',
|
||||||
@@ -250,8 +471,8 @@ function DataList() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const activeFilterCount = useMemo(
|
const activeFilterCount = useMemo(
|
||||||
() => [sourceFilter, typeFilter, countryFilter, searchText.trim()].filter(Boolean).length,
|
() => [sourceFilter.length > 0, typeFilter.length > 0, searchText.trim()].filter(Boolean).length,
|
||||||
[sourceFilter, typeFilter, countryFilter, searchText]
|
[sourceFilter, typeFilter, searchText]
|
||||||
)
|
)
|
||||||
|
|
||||||
const summaryItems = useMemo(() => {
|
const summaryItems = useMemo(() => {
|
||||||
@@ -281,30 +502,24 @@ function DataList() {
|
|||||||
return 4
|
return 4
|
||||||
}, [isCompact, leftPanelWidth])
|
}, [isCompact, leftPanelWidth])
|
||||||
|
|
||||||
const treemapRowHeight = useMemo(() => {
|
|
||||||
if (isCompact) return 88
|
|
||||||
if (leftPanelWidth < 360) return 44
|
|
||||||
if (leftPanelWidth < 520) return 48
|
|
||||||
return 56
|
|
||||||
}, [isCompact, leftPanelWidth])
|
|
||||||
|
|
||||||
const treemapItems = useMemo(() => {
|
const treemapItems = useMemo(() => {
|
||||||
const palette = ['ocean', 'sky', 'mint', 'amber', 'rose', 'violet', 'slate']
|
const palette = ['ocean', 'sky', 'mint', 'amber', 'rose', 'violet', 'slate']
|
||||||
const maxValue = Math.max(...summaryItems.map((item) => item.value), 1)
|
const maxValue = Math.max(...summaryItems.map((item) => item.value), 1)
|
||||||
const allowTallTiles = !isCompact && leftPanelWidth >= 520
|
const allowFeaturedTile = !isCompact && treemapColumns > 1 && summaryItems.length > 2
|
||||||
|
const allowSecondaryTallTiles = !isCompact && leftPanelWidth >= 520
|
||||||
|
|
||||||
return summaryItems.map((item, index) => {
|
return summaryItems.map((item, index) => {
|
||||||
const ratio = item.value / maxValue
|
const ratio = item.value / maxValue
|
||||||
let colSpan = 1
|
let colSpan = 1
|
||||||
let rowSpan = 1
|
let rowSpan = 1
|
||||||
|
|
||||||
if (allowTallTiles && index === 0) {
|
if (allowFeaturedTile && index === 0) {
|
||||||
colSpan = Math.min(2, treemapColumns)
|
colSpan = Math.min(2, treemapColumns)
|
||||||
rowSpan = 2
|
rowSpan = 2
|
||||||
} else if (allowTallTiles && ratio >= 0.7) {
|
} else if (allowSecondaryTallTiles && ratio >= 0.7) {
|
||||||
colSpan = Math.min(2, treemapColumns)
|
colSpan = Math.min(2, treemapColumns)
|
||||||
rowSpan = 2
|
rowSpan = 2
|
||||||
} else if (allowTallTiles && ratio >= 0.35) {
|
} else if (allowSecondaryTallTiles && ratio >= 0.35) {
|
||||||
rowSpan = 2
|
rowSpan = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -317,27 +532,70 @@ function DataList() {
|
|||||||
})
|
})
|
||||||
}, [summaryItems, isCompact, leftPanelWidth, treemapColumns])
|
}, [summaryItems, isCompact, leftPanelWidth, treemapColumns])
|
||||||
|
|
||||||
|
const treemapRows = useMemo(
|
||||||
|
() => estimateTreemapRows(treemapItems, treemapColumns),
|
||||||
|
[treemapColumns, treemapItems]
|
||||||
|
)
|
||||||
|
|
||||||
|
const treemapGap = isCompact ? 8 : 10
|
||||||
|
const treemapMinRowHeight = isCompact ? 88 : 68
|
||||||
|
const treemapTargetRowHeight = isCompact ? 88 : leftPanelWidth < 360 ? 44 : leftPanelWidth < 520 ? 48 : 56
|
||||||
|
const treemapAvailableHeight = Math.max(summaryBodyHeight, 0)
|
||||||
|
const treemapAutoRowHeight = treemapRows > 0
|
||||||
|
? Math.floor((treemapAvailableHeight - Math.max(0, treemapRows - 1) * treemapGap) / treemapRows)
|
||||||
|
: treemapTargetRowHeight
|
||||||
|
const treemapRowHeight = Math.max(
|
||||||
|
treemapMinRowHeight,
|
||||||
|
Math.min(treemapTargetRowHeight, treemapAutoRowHeight || treemapTargetRowHeight)
|
||||||
|
)
|
||||||
|
const treemapContentHeight = treemapRows * treemapRowHeight + Math.max(0, treemapRows - 1) * treemapGap
|
||||||
|
const treemapTilePadding = treemapRowHeight <= 72 ? 8 : treemapRowHeight <= 84 ? 10 : 12
|
||||||
|
const treemapLabelSize = treemapRowHeight <= 72 ? 10 : treemapRowHeight <= 84 ? 11 : 12
|
||||||
|
const treemapValueSize = treemapRowHeight <= 72 ? 13 : treemapRowHeight <= 84 ? 15 : 16
|
||||||
|
|
||||||
const pageHeight = '100%'
|
const pageHeight = '100%'
|
||||||
const desktopTableHeight = rightColumnHeight - tableHeaderHeight - 132
|
const desktopTableHeight = rightColumnHeight - tableHeaderHeight - 132
|
||||||
const compactTableHeight = mainAreaHeight - tableHeaderHeight - 156
|
const compactTableHeight = mainAreaHeight - tableHeaderHeight - 156
|
||||||
const tableHeight = Math.max(180, isCompact ? compactTableHeight : desktopTableHeight)
|
const tableHeight = Math.max(180, isCompact ? compactTableHeight : desktopTableHeight)
|
||||||
|
|
||||||
|
const detailBaseItems = useMemo(() => {
|
||||||
|
if (!detailData) return []
|
||||||
|
|
||||||
|
return DETAIL_BASE_FIELDS.map((key) => ({
|
||||||
|
key,
|
||||||
|
label: formatFieldLabel(key),
|
||||||
|
value: formatDetailValue(key, detailData[key as keyof CollectedData]),
|
||||||
|
})).filter((item) => item.value !== '-')
|
||||||
|
}, [detailData])
|
||||||
|
|
||||||
|
const detailMetadataItems = useMemo(() => {
|
||||||
|
if (!detailData?.metadata) return []
|
||||||
|
|
||||||
|
return Object.entries(detailData.metadata)
|
||||||
|
.filter(([key]) => key !== '_detail_url')
|
||||||
|
.map(([key, value]) => ({
|
||||||
|
key,
|
||||||
|
label: formatFieldLabel(key),
|
||||||
|
value: formatDetailValue(key, value),
|
||||||
|
isBlock: typeof value === 'object' && value !== null,
|
||||||
|
}))
|
||||||
|
}, [detailData])
|
||||||
|
|
||||||
const splitLayoutStyle = isCompact
|
const splitLayoutStyle = isCompact
|
||||||
? undefined
|
? undefined
|
||||||
: { gridTemplateColumns: `${leftPanelWidth}px 12px minmax(0, 1fr)` }
|
: { gridTemplateColumns: `${leftPanelWidth}px 12px minmax(0, 1fr)` }
|
||||||
|
|
||||||
const columns: ColumnsType<CollectedData> = [
|
const columns: ColumnsType<CollectedData> = [
|
||||||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
|
|
||||||
{
|
{
|
||||||
title: '名称',
|
title: '名称',
|
||||||
dataIndex: 'name',
|
dataIndex: 'name',
|
||||||
key: 'name',
|
key: 'name',
|
||||||
width: 280,
|
width: 320,
|
||||||
ellipsis: true,
|
ellipsis: true,
|
||||||
render: (name: string, record: CollectedData) => (
|
render: (name: string, record: CollectedData) => (
|
||||||
<Tooltip title={name}>
|
<Tooltip title={name}>
|
||||||
<Button type="link" onClick={() => handleViewDetail(record.id)}>
|
<Button type="link" className="data-list-name-link" onClick={() => handleViewDetail(record.id)}>
|
||||||
{name}
|
<NameMarquee text={name} />
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
),
|
),
|
||||||
@@ -346,23 +604,31 @@ function DataList() {
|
|||||||
title: '数据源',
|
title: '数据源',
|
||||||
dataIndex: 'source',
|
dataIndex: 'source',
|
||||||
key: 'source',
|
key: 'source',
|
||||||
width: 170,
|
minWidth: 140,
|
||||||
render: (source: string) => <Tag icon={getSourceIcon(source)}>{source}</Tag>,
|
render: (value: string) => (
|
||||||
|
value ? (
|
||||||
|
<div className="data-list-tag-cell">
|
||||||
|
<Tag color={getSourceTagColor(value)} style={{ marginInlineEnd: 0 }}>
|
||||||
|
{value}
|
||||||
|
</Tag>
|
||||||
|
</div>
|
||||||
|
) : '-'
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '类型',
|
title: '数据类型',
|
||||||
dataIndex: 'data_type',
|
dataIndex: 'data_type',
|
||||||
key: 'data_type',
|
key: 'data_type',
|
||||||
width: 120,
|
minWidth: 140,
|
||||||
render: (type: string) => <Tag color={getTypeColor(type)}>{type}</Tag>,
|
render: (value: string) => (
|
||||||
},
|
value ? (
|
||||||
{ title: '国家/地区', dataIndex: 'country', key: 'country', width: 130, ellipsis: true },
|
<div className="data-list-tag-cell">
|
||||||
{
|
<Tag color={getDataTypeTagColor(value)} style={{ marginInlineEnd: 0 }}>
|
||||||
title: '数值',
|
{value}
|
||||||
dataIndex: 'value',
|
</Tag>
|
||||||
key: 'value',
|
</div>
|
||||||
width: 140,
|
) : '-'
|
||||||
render: (value: string | null, record: CollectedData) => (value ? `${value} ${record.unit || ''}` : '-'),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '采集时间',
|
title: '采集时间',
|
||||||
@@ -371,6 +637,13 @@ function DataList() {
|
|||||||
width: 180,
|
width: 180,
|
||||||
render: (time: string) => new Date(time).toLocaleString('zh-CN'),
|
render: (time: string) => new Date(time).toLocaleString('zh-CN'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: '参考日期',
|
||||||
|
dataIndex: 'reference_date',
|
||||||
|
key: 'reference_date',
|
||||||
|
width: 120,
|
||||||
|
render: (time: string | null) => (time ? new Date(time).toLocaleDateString('zh-CN') : '-'),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
@@ -406,14 +679,21 @@ function DataList() {
|
|||||||
className="data-list-summary-card data-list-summary-card--panel"
|
className="data-list-summary-card data-list-summary-card--panel"
|
||||||
title="数据概览"
|
title="数据概览"
|
||||||
size="small"
|
size="small"
|
||||||
bodyStyle={{ padding: isCompact ? 12 : 16 }}
|
styles={{ body: { padding: isCompact ? 12 : 16 } }}
|
||||||
>
|
>
|
||||||
|
<div ref={summaryBodyRef} className="data-list-summary-card-inner">
|
||||||
<div
|
<div
|
||||||
className="data-list-summary-treemap"
|
className="data-list-summary-treemap"
|
||||||
style={{
|
style={{
|
||||||
gridTemplateColumns: `repeat(${treemapColumns}, minmax(0, 1fr))`,
|
gridTemplateColumns: `repeat(${treemapColumns}, minmax(0, 1fr))`,
|
||||||
gridAutoRows: `minmax(${treemapRowHeight}px, 1fr)`,
|
gridAutoRows: `${treemapRowHeight}px`,
|
||||||
}}
|
gap: treemapGap,
|
||||||
|
minHeight: treemapAvailableHeight > 0 ? Math.min(treemapContentHeight, treemapAvailableHeight) : undefined,
|
||||||
|
height: treemapContentHeight,
|
||||||
|
['--data-list-treemap-tile-padding' as '--data-list-treemap-tile-padding']: `${treemapTilePadding}px`,
|
||||||
|
['--data-list-treemap-label-size' as '--data-list-treemap-label-size']: `${treemapLabelSize}px`,
|
||||||
|
['--data-list-treemap-value-size' as '--data-list-treemap-value-size']: `${treemapValueSize}px`,
|
||||||
|
} as CSSProperties}
|
||||||
>
|
>
|
||||||
{treemapItems.map((item) => (
|
{treemapItems.map((item) => (
|
||||||
<div
|
<div
|
||||||
@@ -436,6 +716,7 @@ function DataList() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{!isCompact && (
|
{!isCompact && (
|
||||||
@@ -449,7 +730,7 @@ function DataList() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div ref={rightColumnRef} className="data-list-right-column">
|
<div ref={rightColumnRef} className="data-list-right-column">
|
||||||
<Card className="data-list-table-shell" bodyStyle={{ padding: 0 }}>
|
<Card className="data-list-table-shell" styles={{ body: { padding: 0 } }}>
|
||||||
<div ref={tableHeaderRef} className="data-list-table-header data-list-table-header--with-filters">
|
<div ref={tableHeaderRef} className="data-list-table-header data-list-table-header--with-filters">
|
||||||
<div className="data-list-table-header-main">
|
<div className="data-list-table-header-main">
|
||||||
<Space size={8} wrap>
|
<Space size={8} wrap>
|
||||||
@@ -468,6 +749,7 @@ function DataList() {
|
|||||||
<Select
|
<Select
|
||||||
size="middle"
|
size="middle"
|
||||||
placeholder="数据源"
|
placeholder="数据源"
|
||||||
|
mode="multiple"
|
||||||
allowClear
|
allowClear
|
||||||
value={sourceFilter}
|
value={sourceFilter}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
@@ -475,11 +757,14 @@ function DataList() {
|
|||||||
setPage(1)
|
setPage(1)
|
||||||
}}
|
}}
|
||||||
options={sources.map((source) => ({ label: source, value: source }))}
|
options={sources.map((source) => ({ label: source, value: source }))}
|
||||||
|
tagRender={(tagProps) => renderFilterTag(tagProps, getSourceTagColor)}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
|
className="data-list-filter-select"
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
size="middle"
|
size="middle"
|
||||||
placeholder="数据类型"
|
placeholder="数据类型"
|
||||||
|
mode="multiple"
|
||||||
allowClear
|
allowClear
|
||||||
value={typeFilter}
|
value={typeFilter}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
@@ -487,23 +772,13 @@ function DataList() {
|
|||||||
setPage(1)
|
setPage(1)
|
||||||
}}
|
}}
|
||||||
options={types.map((type) => ({ label: type, value: type }))}
|
options={types.map((type) => ({ label: type, value: type }))}
|
||||||
|
tagRender={(tagProps) => renderFilterTag(tagProps, getDataTypeTagColor)}
|
||||||
style={{ width: '100%' }}
|
style={{ width: '100%' }}
|
||||||
/>
|
className="data-list-filter-select"
|
||||||
<Select
|
|
||||||
size="middle"
|
|
||||||
placeholder="国家"
|
|
||||||
allowClear
|
|
||||||
value={countryFilter}
|
|
||||||
onChange={(value) => {
|
|
||||||
setCountryFilter(value)
|
|
||||||
setPage(1)
|
|
||||||
}}
|
|
||||||
options={countries.map((country) => ({ label: country, value: country }))}
|
|
||||||
style={{ width: '100%' }}
|
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
size="middle"
|
size="middle"
|
||||||
placeholder="搜索名称"
|
placeholder="搜索名称、描述、元数据等"
|
||||||
value={searchText}
|
value={searchText}
|
||||||
onChange={(event) => setSearchText(event.target.value)}
|
onChange={(event) => setSearchText(event.target.value)}
|
||||||
onPressEnter={handleSearch}
|
onPressEnter={handleSearch}
|
||||||
@@ -516,9 +791,8 @@ function DataList() {
|
|||||||
dataSource={data}
|
dataSource={data}
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
virtual
|
|
||||||
scroll={{ x: 'max-content', y: tableHeight }}
|
scroll={{ x: 'max-content', y: tableHeight }}
|
||||||
tableLayout="fixed"
|
tableLayout="auto"
|
||||||
size={isCompact ? 'small' : 'middle'}
|
size={isCompact ? 'small' : 'middle'}
|
||||||
pagination={{
|
pagination={{
|
||||||
current: page,
|
current: page,
|
||||||
@@ -548,38 +822,65 @@ function DataList() {
|
|||||||
关闭
|
关闭
|
||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
width={700}
|
width={880}
|
||||||
>
|
>
|
||||||
{detailLoading ? (
|
{detailLoading ? (
|
||||||
<div style={{ textAlign: 'center', padding: 40 }}>
|
<div style={{ textAlign: 'center', padding: 40 }}>
|
||||||
<Spin size="large" />
|
<Spin size="large" />
|
||||||
</div>
|
</div>
|
||||||
) : detailData ? (
|
) : detailData ? (
|
||||||
<Descriptions column={2} bordered>
|
<div className="data-list-detail-modal">
|
||||||
<Descriptions.Item label="ID">{detailData.id}</Descriptions.Item>
|
<section className="data-list-detail-section">
|
||||||
<Descriptions.Item label="数据源">{detailData.source}</Descriptions.Item>
|
<div className="data-list-detail-hero">
|
||||||
<Descriptions.Item label="数据类型">{detailData.data_type}</Descriptions.Item>
|
<Text className="data-list-detail-hero__label">名称</Text>
|
||||||
<Descriptions.Item label="原始ID">{detailData.source_id || '-'}</Descriptions.Item>
|
<Title level={5} className="data-list-detail-hero__title">
|
||||||
<Descriptions.Item label="名称" span={2}>{detailData.name}</Descriptions.Item>
|
{detailData.name || '-'}
|
||||||
<Descriptions.Item label="标题" span={2}>{detailData.title || '-'}</Descriptions.Item>
|
</Title>
|
||||||
<Descriptions.Item label="描述" span={2}>{detailData.description || '-'}</Descriptions.Item>
|
</div>
|
||||||
<Descriptions.Item label="国家">{detailData.country || '-'}</Descriptions.Item>
|
</section>
|
||||||
<Descriptions.Item label="城市">{detailData.city || '-'}</Descriptions.Item>
|
|
||||||
<Descriptions.Item label="经度">{detailData.longitude || '-'}</Descriptions.Item>
|
{detailBaseItems.length > 0 && (
|
||||||
<Descriptions.Item label="纬度">{detailData.latitude || '-'}</Descriptions.Item>
|
<section className="data-list-detail-section">
|
||||||
<Descriptions.Item label="数值">{detailData.value} {detailData.unit || ''}</Descriptions.Item>
|
<Text strong className="data-list-detail-section__title">基础信息</Text>
|
||||||
<Descriptions.Item label="采集时间">
|
<div className="data-list-detail-grid">
|
||||||
{new Date(detailData.collected_at).toLocaleString('zh-CN')}
|
{detailBaseItems.map((item) => (
|
||||||
</Descriptions.Item>
|
<div key={item.key} className="data-list-detail-cell">
|
||||||
<Descriptions.Item label="参考日期">
|
<Text className="data-list-detail-cell__label">{item.label}</Text>
|
||||||
{detailData.reference_date ? new Date(detailData.reference_date).toLocaleDateString('zh-CN') : '-'}
|
<div className="data-list-detail-cell__value">{item.value}</div>
|
||||||
</Descriptions.Item>
|
</div>
|
||||||
<Descriptions.Item label="元数据" span={2}>
|
))}
|
||||||
<pre style={{ margin: 0, maxHeight: 200, overflow: 'auto' }}>
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{detailMetadataItems.length > 0 && (
|
||||||
|
<section className="data-list-detail-section">
|
||||||
|
<Text strong className="data-list-detail-section__title">扩展字段</Text>
|
||||||
|
<div className="data-list-detail-grid">
|
||||||
|
{detailMetadataItems.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.key}
|
||||||
|
className={`data-list-detail-cell${item.isBlock ? ' data-list-detail-cell--block' : ''}`}
|
||||||
|
>
|
||||||
|
<Text className="data-list-detail-cell__label">{item.label}</Text>
|
||||||
|
{item.isBlock ? (
|
||||||
|
<pre className="data-list-detail-code">{item.value}</pre>
|
||||||
|
) : (
|
||||||
|
<div className="data-list-detail-cell__value">{item.value}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<section className="data-list-detail-section">
|
||||||
|
<Text strong className="data-list-detail-section__title">原始元数据</Text>
|
||||||
|
<pre className="data-list-detail-code data-list-detail-code--raw">
|
||||||
{JSON.stringify(detailData.metadata || {}, null, 2)}
|
{JSON.stringify(detailData.metadata || {}, null, 2)}
|
||||||
</pre>
|
</pre>
|
||||||
</Descriptions.Item>
|
</section>
|
||||||
</Descriptions>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Empty description="暂无数据" />
|
<Empty description="暂无数据" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
PlayCircleOutlined, PauseCircleOutlined, PlusOutlined,
|
PlayCircleOutlined, PauseCircleOutlined, PlusOutlined,
|
||||||
EditOutlined, DeleteOutlined, ApiOutlined,
|
EditOutlined, DeleteOutlined, ApiOutlined,
|
||||||
CheckCircleOutlined, CloseCircleOutlined, ExperimentOutlined,
|
CheckCircleOutlined, CloseCircleOutlined, ExperimentOutlined,
|
||||||
SyncOutlined, ClearOutlined
|
SyncOutlined, ClearOutlined, CopyOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import AppLayout from '../../components/AppLayout/AppLayout'
|
import AppLayout from '../../components/AppLayout/AppLayout'
|
||||||
@@ -18,16 +18,28 @@ interface BuiltInDataSource {
|
|||||||
module: string
|
module: string
|
||||||
priority: string
|
priority: string
|
||||||
frequency: string
|
frequency: string
|
||||||
|
endpoint?: string
|
||||||
is_active: boolean
|
is_active: boolean
|
||||||
collector_class: string
|
collector_class: string
|
||||||
last_run: string | null
|
last_run: string | null
|
||||||
is_running: boolean
|
is_running: boolean
|
||||||
task_id: number | null
|
task_id: number | null
|
||||||
progress: number | null
|
progress: number | null
|
||||||
|
phase?: string | null
|
||||||
records_processed: number | null
|
records_processed: number | null
|
||||||
total_records: number | null
|
total_records: number | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface TaskTrackerState {
|
||||||
|
task_id: number | null
|
||||||
|
is_running: boolean
|
||||||
|
progress: number
|
||||||
|
phase: string | null
|
||||||
|
status?: string | null
|
||||||
|
records_processed?: number | null
|
||||||
|
total_records?: number | null
|
||||||
|
}
|
||||||
|
|
||||||
interface CustomDataSource {
|
interface CustomDataSource {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
@@ -89,7 +101,7 @@ function DataSources() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const [taskProgress, setTaskProgress] = useState<Record<number, { progress: number; is_running: boolean }>>({})
|
const [taskProgress, setTaskProgress] = useState<Record<number, TaskTrackerState>>({})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData()
|
fetchData()
|
||||||
@@ -118,80 +130,85 @@ function DataSources() {
|
|||||||
}, [activeTab, builtInSources.length, customSources.length])
|
}, [activeTab, builtInSources.length, customSources.length])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const runningSources = builtInSources.filter(s => s.is_running)
|
const trackedSources = builtInSources.filter((source) => {
|
||||||
if (runningSources.length === 0) return
|
const trackedTask = taskProgress[source.id]
|
||||||
|
return Boolean((trackedTask?.task_id ?? source.task_id) && (trackedTask?.is_running ?? source.is_running))
|
||||||
|
})
|
||||||
|
|
||||||
|
if (trackedSources.length === 0) return
|
||||||
|
|
||||||
const interval = setInterval(async () => {
|
const interval = setInterval(async () => {
|
||||||
const progressMap: Record<number, { progress: number; is_running: boolean }> = {}
|
const updates: Record<number, TaskTrackerState> = {}
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
runningSources.map(async (source) => {
|
trackedSources.map(async (source) => {
|
||||||
|
const trackedTaskId = taskProgress[source.id]?.task_id ?? source.task_id
|
||||||
|
if (!trackedTaskId) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await axios.get(`/api/v1/datasources/${source.id}/task-status`)
|
const res = await axios.get(`/api/v1/datasources/${source.id}/task-status`, {
|
||||||
progressMap[source.id] = {
|
params: { task_id: trackedTaskId },
|
||||||
|
})
|
||||||
|
updates[source.id] = {
|
||||||
|
task_id: res.data.task_id ?? trackedTaskId,
|
||||||
progress: res.data.progress || 0,
|
progress: res.data.progress || 0,
|
||||||
is_running: res.data.is_running
|
is_running: !!res.data.is_running,
|
||||||
|
phase: res.data.phase || null,
|
||||||
|
status: res.data.status || null,
|
||||||
|
records_processed: res.data.records_processed,
|
||||||
|
total_records: res.data.total_records,
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
progressMap[source.id] = { progress: 0, is_running: false }
|
updates[source.id] = {
|
||||||
|
task_id: trackedTaskId,
|
||||||
|
progress: 0,
|
||||||
|
is_running: false,
|
||||||
|
phase: 'failed',
|
||||||
|
status: 'failed',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
setTaskProgress(prev => ({ ...prev, ...progressMap }))
|
setTaskProgress((prev) => {
|
||||||
|
const next = { ...prev, ...updates }
|
||||||
|
for (const [sourceId, state] of Object.entries(updates)) {
|
||||||
|
if (!state.is_running && state.status !== 'running') {
|
||||||
|
delete next[Number(sourceId)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Object.values(updates).some((state) => !state.is_running)) {
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
}, 2000)
|
}, 2000)
|
||||||
|
|
||||||
return () => clearInterval(interval)
|
return () => clearInterval(interval)
|
||||||
}, [builtInSources.map(s => s.id).join(',')])
|
}, [builtInSources, taskProgress])
|
||||||
|
|
||||||
const handleTrigger = async (id: number) => {
|
const handleTrigger = async (id: number) => {
|
||||||
try {
|
try {
|
||||||
await axios.post(`/api/v1/datasources/${id}/trigger`)
|
const res = await axios.post(`/api/v1/datasources/${id}/trigger`)
|
||||||
message.success('任务已触发')
|
message.success('任务已触发')
|
||||||
// Trigger polling immediately
|
setTaskProgress(prev => ({
|
||||||
setTaskProgress(prev => ({ ...prev, [id]: { progress: 0, is_running: true } }))
|
...prev,
|
||||||
// Also refresh data
|
[id]: {
|
||||||
|
task_id: res.data.task_id ?? null,
|
||||||
|
progress: 0,
|
||||||
|
is_running: true,
|
||||||
|
phase: 'queued',
|
||||||
|
status: 'running',
|
||||||
|
},
|
||||||
|
}))
|
||||||
fetchData()
|
fetchData()
|
||||||
// Also fetch the running task status
|
|
||||||
pollTaskStatus(id)
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const err = error as { response?: { data?: { detail?: string } } }
|
const err = error as { response?: { data?: { detail?: string } } }
|
||||||
message.error(err.response?.data?.detail || '触发失败')
|
message.error(err.response?.data?.detail || '触发失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const pollTaskStatus = async (sourceId: number) => {
|
|
||||||
const poll = async () => {
|
|
||||||
try {
|
|
||||||
const res = await axios.get(`/api/v1/datasources/${sourceId}/task-status`)
|
|
||||||
const data = res.data
|
|
||||||
|
|
||||||
setTaskProgress(prev => ({ ...prev, [sourceId]: {
|
|
||||||
progress: data.progress || 0,
|
|
||||||
is_running: data.is_running
|
|
||||||
} }))
|
|
||||||
|
|
||||||
// Keep polling while running
|
|
||||||
if (data.is_running) {
|
|
||||||
setTimeout(poll, 2000)
|
|
||||||
} else {
|
|
||||||
// Task completed - refresh data and clear this source from progress
|
|
||||||
setTimeout(() => {
|
|
||||||
setTaskProgress(prev => {
|
|
||||||
const newState = { ...prev }
|
|
||||||
delete newState[sourceId]
|
|
||||||
return newState
|
|
||||||
})
|
|
||||||
}, 1000)
|
|
||||||
fetchData()
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Stop polling on error
|
|
||||||
}
|
|
||||||
}
|
|
||||||
poll()
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleToggle = async (id: number, current: boolean) => {
|
const handleToggle = async (id: number, current: boolean) => {
|
||||||
const endpoint = current ? 'disable' : 'enable'
|
const endpoint = current ? 'disable' : 'enable'
|
||||||
try {
|
try {
|
||||||
@@ -229,7 +246,7 @@ function DataSources() {
|
|||||||
name: data.name,
|
name: data.name,
|
||||||
description: null,
|
description: null,
|
||||||
source_type: data.collector_class,
|
source_type: data.collector_class,
|
||||||
endpoint: '',
|
endpoint: data.endpoint || '',
|
||||||
auth_type: 'none',
|
auth_type: 'none',
|
||||||
headers: {},
|
headers: {},
|
||||||
config: {},
|
config: {},
|
||||||
@@ -340,6 +357,27 @@ function DataSources() {
|
|||||||
setTestResult(null)
|
setTestResult(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleCopyLink = async (value: string, successText: string) => {
|
||||||
|
try {
|
||||||
|
if (navigator.clipboard?.writeText) {
|
||||||
|
await navigator.clipboard.writeText(value)
|
||||||
|
} else {
|
||||||
|
const textArea = document.createElement('textarea')
|
||||||
|
textArea.value = value
|
||||||
|
textArea.style.position = 'fixed'
|
||||||
|
textArea.style.opacity = '0'
|
||||||
|
document.body.appendChild(textArea)
|
||||||
|
textArea.focus()
|
||||||
|
textArea.select()
|
||||||
|
document.execCommand('copy')
|
||||||
|
document.body.removeChild(textArea)
|
||||||
|
}
|
||||||
|
message.success(successText)
|
||||||
|
} catch {
|
||||||
|
message.error('复制失败,请手动复制')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const builtinColumns = [
|
const builtinColumns = [
|
||||||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 60, fixed: 'left' as const },
|
{ title: 'ID', dataIndex: 'id', key: 'id', width: 60, fixed: 'left' as const },
|
||||||
{
|
{
|
||||||
@@ -374,15 +412,31 @@ function DataSources() {
|
|||||||
title: '状态',
|
title: '状态',
|
||||||
dataIndex: 'is_active',
|
dataIndex: 'is_active',
|
||||||
key: 'is_active',
|
key: 'is_active',
|
||||||
width: 100,
|
width: 180,
|
||||||
render: (_: unknown, record: BuiltInDataSource) => {
|
render: (_: unknown, record: BuiltInDataSource) => {
|
||||||
const progress = taskProgress[record.id]
|
const taskState = taskProgress[record.id]
|
||||||
if (progress?.is_running || record.is_running) {
|
const isTaskRunning = taskState?.is_running || record.is_running
|
||||||
const pct = progress?.progress ?? record.progress ?? 0
|
|
||||||
|
const phaseLabelMap: Record<string, string> = {
|
||||||
|
queued: '排队中',
|
||||||
|
fetching: '抓取中',
|
||||||
|
transforming: '处理中',
|
||||||
|
saving: '保存中',
|
||||||
|
completed: '已完成',
|
||||||
|
failed: '失败',
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isTaskRunning) {
|
||||||
|
const pct = taskState?.progress ?? record.progress ?? 0
|
||||||
|
const phase = taskState?.phase || record.phase || 'queued'
|
||||||
return (
|
return (
|
||||||
<Tag color="blue">
|
<Space size={6} wrap>
|
||||||
采集中 {Math.round(pct)}%
|
<Tag color={record.is_active ? 'green' : 'red'}>{record.is_active ? '运行中' : '已暂停'}</Tag>
|
||||||
|
<Tag color="processing">
|
||||||
|
{phaseLabelMap[phase] || phase}
|
||||||
|
{pct > 0 ? ` ${Math.round(pct)}%` : ''}
|
||||||
</Tag>
|
</Tag>
|
||||||
|
</Space>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return <Tag color={record.is_active ? 'green' : 'red'}>{record.is_active ? '运行中' : '已暂停'}</Tag>
|
return <Tag color={record.is_active ? 'green' : 'red'}>{record.is_active ? '运行中' : '已暂停'}</Tag>
|
||||||
@@ -420,6 +474,22 @@ function DataSources() {
|
|||||||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 60, fixed: 'left' as const },
|
{ title: 'ID', dataIndex: 'id', key: 'id', width: 60, fixed: 'left' as const },
|
||||||
{ title: '名称', dataIndex: 'name', key: 'name', width: 150, ellipsis: true },
|
{ title: '名称', dataIndex: 'name', key: 'name', width: 150, ellipsis: true },
|
||||||
{ title: '类型', dataIndex: 'source_type', key: 'source_type', width: 100 },
|
{ title: '类型', dataIndex: 'source_type', key: 'source_type', width: 100 },
|
||||||
|
{
|
||||||
|
title: 'API链接',
|
||||||
|
dataIndex: 'endpoint',
|
||||||
|
key: 'endpoint',
|
||||||
|
width: 280,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (endpoint: string) => (
|
||||||
|
endpoint ? (
|
||||||
|
<Tooltip title={endpoint}>
|
||||||
|
<a href={endpoint} target="_blank" rel="noreferrer">
|
||||||
|
{endpoint}
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
) : '-'
|
||||||
|
),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: '状态',
|
title: '状态',
|
||||||
dataIndex: 'is_active',
|
dataIndex: 'is_active',
|
||||||
@@ -477,7 +547,6 @@ function DataSources() {
|
|||||||
scroll={{ x: 800, y: builtinTableHeight }}
|
scroll={{ x: 800, y: builtinTableHeight }}
|
||||||
tableLayout="fixed"
|
tableLayout="fixed"
|
||||||
size="small"
|
size="small"
|
||||||
virtual
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -509,10 +578,9 @@ function DataSources() {
|
|||||||
rowKey="id"
|
rowKey="id"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
scroll={{ x: 600, y: customTableHeight }}
|
scroll={{ x: 900, y: customTableHeight }}
|
||||||
tableLayout="fixed"
|
tableLayout="fixed"
|
||||||
size="small"
|
size="small"
|
||||||
virtual
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -811,6 +879,19 @@ function DataSources() {
|
|||||||
<Input value={viewingSource.frequency} disabled />
|
<Input value={viewingSource.frequency} disabled />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|
||||||
|
<Form.Item label="采集源 API 链接">
|
||||||
|
<Space.Compact style={{ width: '100%' }}>
|
||||||
|
<Input value={viewingSource.endpoint || '-'} readOnly />
|
||||||
|
<Tooltip title={viewingSource.endpoint ? '复制采集源 API 链接' : '当前没有可复制的采集源 API 链接'}>
|
||||||
|
<Button
|
||||||
|
disabled={!viewingSource.endpoint}
|
||||||
|
icon={<CopyOutlined />}
|
||||||
|
onClick={() => viewingSource.endpoint && handleCopyLink(viewingSource.endpoint, '采集源 API 链接已复制')}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Space.Compact>
|
||||||
|
</Form.Item>
|
||||||
|
|
||||||
<Collapse
|
<Collapse
|
||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useRef, useState } from 'react'
|
import { useEffect, useRef, useState, type ReactNode } from 'react'
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
@@ -55,6 +55,22 @@ interface CollectorSettings {
|
|||||||
next_run_at: string | null
|
next_run_at: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SettingsPanel({
|
||||||
|
loading,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
loading: boolean
|
||||||
|
children: ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="settings-pane">
|
||||||
|
<Card className="settings-panel-card" loading={loading}>
|
||||||
|
<div className="settings-panel-scroll">{children}</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function Settings() {
|
function Settings() {
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [savingCollectorId, setSavingCollectorId] = useState<number | null>(null)
|
const [savingCollectorId, setSavingCollectorId] = useState<number | null>(null)
|
||||||
@@ -227,7 +243,7 @@ function Settings() {
|
|||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
width: 120,
|
width: 92,
|
||||||
fixed: 'right' as const,
|
fixed: 'right' as const,
|
||||||
render: (_: unknown, record: CollectorSettings) => (
|
render: (_: unknown, record: CollectorSettings) => (
|
||||||
<Button type="primary" loading={savingCollectorId === record.id} onClick={() => saveCollector(record)}>
|
<Button type="primary" loading={savingCollectorId === record.id} onClick={() => saveCollector(record)}>
|
||||||
@@ -237,27 +253,12 @@ function Settings() {
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
const tabItems = [
|
||||||
<AppLayout>
|
|
||||||
<div className="page-shell settings-shell">
|
|
||||||
<div className="page-shell__header">
|
|
||||||
<div>
|
|
||||||
<Title level={3} style={{ marginBottom: 4 }}>系统配置中心</Title>
|
|
||||||
<Text type="secondary">这一页现在已经直接连接数据库配置和采集调度,不再只是演示表单。</Text>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="page-shell__body settings-tabs-shell">
|
|
||||||
<Tabs
|
|
||||||
className="settings-tabs"
|
|
||||||
items={[
|
|
||||||
{
|
{
|
||||||
key: 'system',
|
key: 'system',
|
||||||
label: '系统显示',
|
label: '系统显示',
|
||||||
children: (
|
children: (
|
||||||
<div className="settings-tab-panel">
|
<SettingsPanel loading={loading}>
|
||||||
<Card className="settings-panel-card" loading={loading}>
|
|
||||||
<div className="settings-panel-scroll">
|
|
||||||
<Form form={systemForm} layout="vertical" onFinish={(values) => saveSection('system', values)}>
|
<Form form={systemForm} layout="vertical" onFinish={(values) => saveSection('system', values)}>
|
||||||
<Form.Item name="system_name" label="系统名称" rules={[{ required: true, message: '请输入系统名称' }]}>
|
<Form.Item name="system_name" label="系统名称" rules={[{ required: true, message: '请输入系统名称' }]}>
|
||||||
<Input />
|
<Input />
|
||||||
@@ -276,18 +277,14 @@ function Settings() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Button type="primary" htmlType="submit">保存系统配置</Button>
|
<Button type="primary" htmlType="submit">保存系统配置</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</SettingsPanel>
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'notifications',
|
key: 'notifications',
|
||||||
label: '通知策略',
|
label: '通知策略',
|
||||||
children: (
|
children: (
|
||||||
<div className="settings-tab-panel">
|
<SettingsPanel loading={loading}>
|
||||||
<Card className="settings-panel-card" loading={loading}>
|
|
||||||
<div className="settings-panel-scroll">
|
|
||||||
<Form form={notificationForm} layout="vertical" onFinish={(values) => saveSection('notifications', values)}>
|
<Form form={notificationForm} layout="vertical" onFinish={(values) => saveSection('notifications', values)}>
|
||||||
<Form.Item name="email_enabled" label="启用邮件通知" valuePropName="checked">
|
<Form.Item name="email_enabled" label="启用邮件通知" valuePropName="checked">
|
||||||
<Switch />
|
<Switch />
|
||||||
@@ -306,18 +303,14 @@ function Settings() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Button type="primary" htmlType="submit">保存通知配置</Button>
|
<Button type="primary" htmlType="submit">保存通知配置</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</SettingsPanel>
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'security',
|
key: 'security',
|
||||||
label: '安全策略',
|
label: '安全策略',
|
||||||
children: (
|
children: (
|
||||||
<div className="settings-tab-panel">
|
<SettingsPanel loading={loading}>
|
||||||
<Card className="settings-panel-card" loading={loading}>
|
|
||||||
<div className="settings-panel-scroll">
|
|
||||||
<Form form={securityForm} layout="vertical" onFinish={(values) => saveSection('security', values)}>
|
<Form form={securityForm} layout="vertical" onFinish={(values) => saveSection('security', values)}>
|
||||||
<Form.Item name="session_timeout" label="会话超时(分钟)">
|
<Form.Item name="session_timeout" label="会话超时(分钟)">
|
||||||
<InputNumber min={5} max={1440} style={{ width: '100%' }} />
|
<InputNumber min={5} max={1440} style={{ width: '100%' }} />
|
||||||
@@ -336,41 +329,48 @@ function Settings() {
|
|||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Button type="primary" htmlType="submit">保存安全配置</Button>
|
<Button type="primary" htmlType="submit">保存安全配置</Button>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</SettingsPanel>
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'collectors',
|
key: 'collectors',
|
||||||
label: '采集调度',
|
label: '采集调度',
|
||||||
children: (
|
children: (
|
||||||
<div className="settings-tab-panel">
|
<div className="settings-pane">
|
||||||
<Card
|
<Card
|
||||||
className="settings-panel-card settings-panel-card--table"
|
className="settings-panel-card settings-panel-card--table"
|
||||||
loading={loading}
|
loading={loading}
|
||||||
bodyStyle={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
|
styles={{ body: { padding: 0 } }}
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref={collectorTableRegionRef}
|
|
||||||
className="table-scroll-region data-source-table-region settings-table-scroll-region"
|
|
||||||
style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}
|
|
||||||
>
|
>
|
||||||
|
<div ref={collectorTableRegionRef} className="table-scroll-region data-source-table-region">
|
||||||
<Table
|
<Table
|
||||||
rowKey="id"
|
rowKey="id"
|
||||||
columns={collectorColumns}
|
columns={collectorColumns}
|
||||||
dataSource={collectors}
|
dataSource={collectors}
|
||||||
pagination={false}
|
pagination={false}
|
||||||
scroll={{ x: 1200, y: collectorTableHeight }}
|
scroll={{ x: 1200, y: collectorTableHeight }}
|
||||||
virtual
|
tableLayout="fixed"
|
||||||
|
size="small"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
]}
|
]
|
||||||
/>
|
|
||||||
|
return (
|
||||||
|
<AppLayout>
|
||||||
|
<div className="page-shell settings-shell">
|
||||||
|
<div className="page-shell__header">
|
||||||
|
<div>
|
||||||
|
<Title level={3} style={{ marginBottom: 4 }}>系统配置中心</Title>
|
||||||
|
<Text type="secondary">这一页现在已经直接连接数据库配置和采集调度,不再只是演示表单。</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="page-shell__body settings-tabs-shell">
|
||||||
|
<Tabs className="settings-tabs" items={tabItems} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AppLayout>
|
</AppLayout>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useRef, useState } from 'react'
|
||||||
import { Table, Button, Tag, Space, message, Modal, Form, Input, Select } from 'antd'
|
import { Table, Button, Tag, Space, message, Modal, Form, Input, Select } from 'antd'
|
||||||
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'
|
import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
@@ -18,6 +18,8 @@ function Users() {
|
|||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [modalVisible, setModalVisible] = useState(false)
|
const [modalVisible, setModalVisible] = useState(false)
|
||||||
const [editingUser, setEditingUser] = useState<User | null>(null)
|
const [editingUser, setEditingUser] = useState<User | null>(null)
|
||||||
|
const tableRegionRef = useRef<HTMLDivElement | null>(null)
|
||||||
|
const [tableHeight, setTableHeight] = useState(360)
|
||||||
const [form] = Form.useForm()
|
const [form] = Form.useForm()
|
||||||
|
|
||||||
const fetchUsers = async () => {
|
const fetchUsers = async () => {
|
||||||
@@ -34,6 +36,24 @@ function Users() {
|
|||||||
fetchUsers()
|
fetchUsers()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const updateTableHeight = () => {
|
||||||
|
const regionHeight = tableRegionRef.current?.offsetHeight || 0
|
||||||
|
setTableHeight(Math.max(220, regionHeight - 56))
|
||||||
|
}
|
||||||
|
|
||||||
|
updateTableHeight()
|
||||||
|
|
||||||
|
if (typeof ResizeObserver === 'undefined') {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new ResizeObserver(updateTableHeight)
|
||||||
|
if (tableRegionRef.current) observer.observe(tableRegionRef.current)
|
||||||
|
|
||||||
|
return () => observer.disconnect()
|
||||||
|
}, [users.length])
|
||||||
|
|
||||||
const handleAdd = () => {
|
const handleAdd = () => {
|
||||||
setEditingUser(null)
|
setEditingUser(null)
|
||||||
form.resetFields()
|
form.resetFields()
|
||||||
@@ -77,12 +97,13 @@ function Users() {
|
|||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
|
{ title: 'ID', dataIndex: 'id', key: 'id', width: 80 },
|
||||||
{ title: '用户名', dataIndex: 'username', key: 'username' },
|
{ title: '用户名', dataIndex: 'username', key: 'username', width: 180 },
|
||||||
{ title: '邮箱', dataIndex: 'email', key: 'email' },
|
{ title: '邮箱', dataIndex: 'email', key: 'email', width: 260, ellipsis: true },
|
||||||
{
|
{
|
||||||
title: '角色',
|
title: '角色',
|
||||||
dataIndex: 'role',
|
dataIndex: 'role',
|
||||||
key: 'role',
|
key: 'role',
|
||||||
|
width: 140,
|
||||||
render: (role: string) => {
|
render: (role: string) => {
|
||||||
const colors: Record<string, string> = {
|
const colors: Record<string, string> = {
|
||||||
super_admin: 'red',
|
super_admin: 'red',
|
||||||
@@ -97,6 +118,7 @@ function Users() {
|
|||||||
title: '状态',
|
title: '状态',
|
||||||
dataIndex: 'is_active',
|
dataIndex: 'is_active',
|
||||||
key: 'is_active',
|
key: 'is_active',
|
||||||
|
width: 120,
|
||||||
render: (active: boolean) => (
|
render: (active: boolean) => (
|
||||||
<Tag color={active ? 'green' : 'red'}>{active ? '活跃' : '禁用'}</Tag>
|
<Tag color={active ? 'green' : 'red'}>{active ? '活跃' : '禁用'}</Tag>
|
||||||
),
|
),
|
||||||
@@ -104,6 +126,7 @@ function Users() {
|
|||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
|
width: 180,
|
||||||
render: (_: unknown, record: User) => (
|
render: (_: unknown, record: User) => (
|
||||||
<Space>
|
<Space>
|
||||||
<Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)}>编辑</Button>
|
<Button type="link" icon={<EditOutlined />} onClick={() => handleEdit(record)}>编辑</Button>
|
||||||
@@ -121,8 +144,15 @@ function Users() {
|
|||||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>添加用户</Button>
|
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>添加用户</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="page-shell__body">
|
<div className="page-shell__body">
|
||||||
<div className="table-scroll-region" style={{ height: '100%' }}>
|
<div ref={tableRegionRef} className="table-scroll-region data-source-table-region" style={{ height: '100%' }}>
|
||||||
<Table columns={columns} dataSource={users} rowKey="id" loading={loading} scroll={{ x: 'max-content', y: 'calc(100% - 72px)' }} tableLayout="fixed" />
|
<Table
|
||||||
|
columns={columns}
|
||||||
|
dataSource={users}
|
||||||
|
rowKey="id"
|
||||||
|
loading={loading}
|
||||||
|
scroll={{ x: 'max-content', y: tableHeight }}
|
||||||
|
tableLayout="fixed"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
24
planet.sh
24
planet.sh
@@ -11,6 +11,27 @@ YELLOW='\033[1;33m'
|
|||||||
BLUE='\033[0;34m'
|
BLUE='\033[0;34m'
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
|
ensure_uv_backend_deps() {
|
||||||
|
echo -e "${BLUE}📦 检查后端 uv 环境...${NC}"
|
||||||
|
|
||||||
|
if ! command -v uv >/dev/null 2>&1; then
|
||||||
|
echo -e "${RED}❌ 未找到 uv,请先安装 uv 并加入 PATH${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
cd "$SCRIPT_DIR"
|
||||||
|
|
||||||
|
if [ ! -x "$SCRIPT_DIR/.venv/bin/python" ]; then
|
||||||
|
echo -e "${YELLOW}⚠️ 未检测到 .venv,正在执行 uv sync...${NC}"
|
||||||
|
uv sync --group dev
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -x "$SCRIPT_DIR/.venv/bin/python" ]; then
|
||||||
|
echo -e "${RED}❌ uv 环境初始化失败,未找到 .venv/bin/python${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
ensure_frontend_deps() {
|
ensure_frontend_deps() {
|
||||||
echo -e "${BLUE}📦 检查前端依赖...${NC}"
|
echo -e "${BLUE}📦 检查前端依赖...${NC}"
|
||||||
|
|
||||||
@@ -41,9 +62,10 @@ start() {
|
|||||||
sleep 3
|
sleep 3
|
||||||
|
|
||||||
echo -e "${BLUE}🔧 启动后端...${NC}"
|
echo -e "${BLUE}🔧 启动后端...${NC}"
|
||||||
|
ensure_uv_backend_deps
|
||||||
pkill -f "uvicorn" 2>/dev/null || true
|
pkill -f "uvicorn" 2>/dev/null || true
|
||||||
cd "$SCRIPT_DIR/backend"
|
cd "$SCRIPT_DIR/backend"
|
||||||
PYTHONPATH="$SCRIPT_DIR/backend" nohup python3 -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload > /tmp/planet_backend.log 2>&1 &
|
PYTHONPATH="$SCRIPT_DIR/backend" nohup uv run --project "$SCRIPT_DIR" python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload > /tmp/planet_backend.log 2>&1 &
|
||||||
BACKEND_PID=$!
|
BACKEND_PID=$!
|
||||||
|
|
||||||
sleep 3
|
sleep 3
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
name = "planet"
|
name = "planet"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
description = "智能星球计划 - 态势感知系统"
|
description = "智能星球计划 - 态势感知系统"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.14"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"fastapi>=0.109.0",
|
"fastapi>=0.109.0",
|
||||||
"uvicorn[standard]>=0.27.0",
|
"uvicorn[standard]>=0.27.0",
|
||||||
@@ -13,28 +13,32 @@ dependencies = [
|
|||||||
"pydantic-settings>=2.1.0",
|
"pydantic-settings>=2.1.0",
|
||||||
"python-jose[cryptography]>=3.3.0",
|
"python-jose[cryptography]>=3.3.0",
|
||||||
"bcrypt>=4.0.0",
|
"bcrypt>=4.0.0",
|
||||||
|
"passlib[bcrypt]>=1.7.4",
|
||||||
"python-multipart>=0.0.6",
|
"python-multipart>=0.0.6",
|
||||||
"httpx>=0.26.0",
|
"httpx>=0.26.0",
|
||||||
|
"beautifulsoup4>=4.12.0",
|
||||||
"aiofiles>=23.2.1",
|
"aiofiles>=23.2.1",
|
||||||
"python-dotenv>=1.0.0",
|
"python-dotenv>=1.0.0",
|
||||||
"email-validator>=2.1.0",
|
"email-validator>=2.1.0",
|
||||||
|
"apscheduler>=3.10.4",
|
||||||
|
"networkx>=3.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.uv]
|
[tool.uv]
|
||||||
package = false
|
package = false
|
||||||
|
|
||||||
[scripts]
|
[dependency-groups]
|
||||||
start = "uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload"
|
dev = [
|
||||||
start-prod = "uvicorn app.main:app --host 0.0.0.0 --port 8000"
|
"black>=24.0.0",
|
||||||
init-db = "python scripts/init_db.py"
|
"pytest>=7.4.0",
|
||||||
lint = "ruff check ."
|
"pytest-asyncio>=0.23.0",
|
||||||
format = "black ."
|
"ruff>=0.6.0",
|
||||||
test = "pytest"
|
]
|
||||||
|
|
||||||
[tool.black]
|
[tool.black]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
target-version = ["py311"]
|
target-version = ["py314"]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
line-length = 100
|
line-length = 100
|
||||||
target-version = "py311"
|
target-version = "py314"
|
||||||
|
|||||||
57
scripts/backfill_collected_data_metadata.py
Normal file
57
scripts/backfill_collected_data_metadata.py
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Backfill legacy collected_data columns into metadata."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
BACKEND_DIR = os.path.join(ROOT_DIR, "backend")
|
||||||
|
|
||||||
|
sys.path.insert(0, ROOT_DIR)
|
||||||
|
sys.path.insert(0, BACKEND_DIR)
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
from app.core.collected_data_fields import build_dynamic_metadata
|
||||||
|
from app.models.collected_data import CollectedData
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
database_url = os.environ.get(
|
||||||
|
"DATABASE_URL", "postgresql+asyncpg://postgres:postgres@localhost:5432/planet_db"
|
||||||
|
)
|
||||||
|
engine = create_async_engine(database_url, echo=False)
|
||||||
|
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||||
|
|
||||||
|
updated = 0
|
||||||
|
|
||||||
|
async with async_session() as session:
|
||||||
|
result = await session.execute(select(CollectedData))
|
||||||
|
records = result.scalars().all()
|
||||||
|
|
||||||
|
for record in records:
|
||||||
|
merged_metadata = build_dynamic_metadata(
|
||||||
|
record.extra_data or {},
|
||||||
|
country=record.country,
|
||||||
|
city=record.city,
|
||||||
|
latitude=record.latitude,
|
||||||
|
longitude=record.longitude,
|
||||||
|
value=record.value,
|
||||||
|
unit=record.unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
if merged_metadata != (record.extra_data or {}):
|
||||||
|
record.extra_data = merged_metadata
|
||||||
|
updated += 1
|
||||||
|
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
await engine.dispose()
|
||||||
|
print(f"Backfill completed. Updated {updated} collected_data rows.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
119
scripts/check_collected_data_column_removal_ready.py
Normal file
119
scripts/check_collected_data_column_removal_ready.py
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Check whether collected_data is ready for strong-coupled column removal."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
ROOT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
BACKEND_DIR = os.path.join(ROOT_DIR, "backend")
|
||||||
|
|
||||||
|
sys.path.insert(0, ROOT_DIR)
|
||||||
|
sys.path.insert(0, BACKEND_DIR)
|
||||||
|
|
||||||
|
from sqlalchemy import text
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
|
||||||
|
CHECKS = {
|
||||||
|
"country_missing_in_metadata": """
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM collected_data
|
||||||
|
WHERE country IS NOT NULL
|
||||||
|
AND country != ''
|
||||||
|
AND COALESCE(metadata->>'country', '') = ''
|
||||||
|
""",
|
||||||
|
"city_missing_in_metadata": """
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM collected_data
|
||||||
|
WHERE city IS NOT NULL
|
||||||
|
AND city != ''
|
||||||
|
AND COALESCE(metadata->>'city', '') = ''
|
||||||
|
""",
|
||||||
|
"latitude_missing_in_metadata": """
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM collected_data
|
||||||
|
WHERE latitude IS NOT NULL
|
||||||
|
AND latitude != ''
|
||||||
|
AND COALESCE(metadata->>'latitude', '') = ''
|
||||||
|
""",
|
||||||
|
"longitude_missing_in_metadata": """
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM collected_data
|
||||||
|
WHERE longitude IS NOT NULL
|
||||||
|
AND longitude != ''
|
||||||
|
AND COALESCE(metadata->>'longitude', '') = ''
|
||||||
|
""",
|
||||||
|
"value_missing_in_metadata": """
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM collected_data
|
||||||
|
WHERE value IS NOT NULL
|
||||||
|
AND value != ''
|
||||||
|
AND COALESCE(metadata->>'value', '') = ''
|
||||||
|
""",
|
||||||
|
"unit_missing_in_metadata": """
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM collected_data
|
||||||
|
WHERE unit IS NOT NULL
|
||||||
|
AND unit != ''
|
||||||
|
AND COALESCE(metadata->>'unit', '') = ''
|
||||||
|
""",
|
||||||
|
"rows_with_any_legacy_value": """
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM collected_data
|
||||||
|
WHERE COALESCE(country, '') != ''
|
||||||
|
OR COALESCE(city, '') != ''
|
||||||
|
OR COALESCE(latitude, '') != ''
|
||||||
|
OR COALESCE(longitude, '') != ''
|
||||||
|
OR COALESCE(value, '') != ''
|
||||||
|
OR COALESCE(unit, '') != ''
|
||||||
|
""",
|
||||||
|
"total_rows": """
|
||||||
|
SELECT COUNT(*) FROM collected_data
|
||||||
|
""",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def scalar(session: AsyncSession, sql: str) -> int:
|
||||||
|
result = await session.execute(text(sql))
|
||||||
|
return int(result.scalar() or 0)
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
database_url = os.environ.get(
|
||||||
|
"DATABASE_URL", "postgresql+asyncpg://postgres:postgres@localhost:5432/planet_db"
|
||||||
|
)
|
||||||
|
engine = create_async_engine(database_url, echo=False)
|
||||||
|
async_session = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||||
|
|
||||||
|
async with async_session() as session:
|
||||||
|
results = {name: await scalar(session, sql) for name, sql in CHECKS.items()}
|
||||||
|
|
||||||
|
await engine.dispose()
|
||||||
|
|
||||||
|
print("Collected Data Column Removal Readiness")
|
||||||
|
print("=" * 44)
|
||||||
|
for key, value in results.items():
|
||||||
|
print(f"{key}: {value}")
|
||||||
|
|
||||||
|
blocking_checks = {
|
||||||
|
key: value
|
||||||
|
for key, value in results.items()
|
||||||
|
if key.endswith("_missing_in_metadata") and value > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
print("\nConclusion:")
|
||||||
|
if blocking_checks:
|
||||||
|
print("NOT READY")
|
||||||
|
print("The following fields still have legacy column values not mirrored into metadata:")
|
||||||
|
for key, value in blocking_checks.items():
|
||||||
|
print(f"- {key}: {value}")
|
||||||
|
else:
|
||||||
|
print("READY FOR COLUMN REMOVAL CHECKPOINT")
|
||||||
|
print("All legacy column values are mirrored into metadata.")
|
||||||
|
print("You can proceed to the SQL migration after one more functional verification round.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
41
scripts/drop_collected_data_legacy_columns.py
Normal file
41
scripts/drop_collected_data_legacy_columns.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
"""Drop legacy collected_data columns after metadata backfill verification."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from sqlalchemy import text
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
BACKEND_DIR = ROOT / "backend"
|
||||||
|
|
||||||
|
for path in (ROOT, BACKEND_DIR):
|
||||||
|
path_str = str(path)
|
||||||
|
if path_str not in sys.path:
|
||||||
|
sys.path.insert(0, path_str)
|
||||||
|
|
||||||
|
from app.db.session import engine # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
DROP_SQL = """
|
||||||
|
ALTER TABLE collected_data
|
||||||
|
DROP COLUMN IF EXISTS country,
|
||||||
|
DROP COLUMN IF EXISTS city,
|
||||||
|
DROP COLUMN IF EXISTS latitude,
|
||||||
|
DROP COLUMN IF EXISTS longitude,
|
||||||
|
DROP COLUMN IF EXISTS value,
|
||||||
|
DROP COLUMN IF EXISTS unit;
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
async with engine.begin() as conn:
|
||||||
|
await conn.execute(text(DROP_SQL))
|
||||||
|
|
||||||
|
print("Dropped legacy collected_data columns: country, city, latitude, longitude, value, unit.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
614
uv.lock
generated
614
uv.lock
generated
@@ -1,6 +1,6 @@
|
|||||||
version = 1
|
version = 1
|
||||||
revision = 3
|
revision = 3
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.14"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aiofiles"
|
name = "aiofiles"
|
||||||
@@ -35,7 +35,6 @@ version = "4.12.1"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "idna" },
|
{ name = "idna" },
|
||||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -43,12 +42,15 @@ wheels = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "async-timeout"
|
name = "apscheduler"
|
||||||
version = "5.0.1"
|
version = "3.11.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a5/ae/136395dfbfe00dfc94da3f3e136d0b13f394cba8f4841120e34226265780/async_timeout-5.0.1.tar.gz", hash = "sha256:d9321a7a3d5a6a5e187e824d2fa0793ce379a202935782d555d6e9d2735677d3", size = 9274, upload-time = "2024-11-06T16:41:39.6Z" }
|
dependencies = [
|
||||||
|
{ name = "tzlocal" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/07/12/3e4389e5920b4c1763390c6d371162f3784f86f85cd6d6c1bfe68eef14e2/apscheduler-3.11.2.tar.gz", hash = "sha256:2a9966b052ec805f020c8c4c3ae6e6a06e24b1bf19f2e11d91d8cca0473eef41", size = 108683, upload-time = "2025-12-22T00:39:34.884Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/fe/ba/e2081de779ca30d473f21f5b30e0e737c438205440784c7dfc81efc2b029/async_timeout-5.0.1-py3-none-any.whl", hash = "sha256:39e3809566ff85354557ec2398b55e096c8364bacac9405a7a1fa429e77fe76c", size = 6233, upload-time = "2024-11-06T16:41:37.9Z" },
|
{ url = "https://files.pythonhosted.org/packages/9f/64/2e54428beba8d9992aa478bb8f6de9e4ecaa5f8f513bcfd567ed7fb0262d/apscheduler-3.11.2-py3-none-any.whl", hash = "sha256:ce005177f741409db4e4dd40a7431b76feb856b9dd69d57e0da49d6715bfd26d", size = 64439, upload-time = "2025-12-22T00:39:33.303Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -57,30 +59,6 @@ version = "0.31.0"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/fe/cc/d18065ce2380d80b1bcce927c24a2642efd38918e33fd724bc4bca904877/asyncpg-0.31.0.tar.gz", hash = "sha256:c989386c83940bfbd787180f2b1519415e2d3d6277a70d9d0f0145ac73500735", size = 993667, upload-time = "2025-11-24T23:27:00.812Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/08/17/cc02bc49bc350623d050fa139e34ea512cd6e020562f2a7312a7bcae4bc9/asyncpg-0.31.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eee690960e8ab85063ba93af2ce128c0f52fd655fdff9fdb1a28df01329f031d", size = 643159, upload-time = "2025-11-24T23:25:36.443Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a4/62/4ded7d400a7b651adf06f49ea8f73100cca07c6df012119594d1e3447aa6/asyncpg-0.31.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2657204552b75f8288de08ca60faf4a99a65deef3a71d1467454123205a88fab", size = 638157, upload-time = "2025-11-24T23:25:37.89Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/5b/4179538a9a72166a0bf60ad783b1ef16efb7960e4d7b9afe9f77a5551680/asyncpg-0.31.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a429e842a3a4b4ea240ea52d7fe3f82d5149853249306f7ff166cb9948faa46c", size = 2918051, upload-time = "2025-11-24T23:25:39.461Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e6/35/c27719ae0536c5b6e61e4701391ffe435ef59539e9360959240d6e47c8c8/asyncpg-0.31.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0807be46c32c963ae40d329b3a686356e417f674c976c07fa49f1b30303f109", size = 2972640, upload-time = "2025-11-24T23:25:41.512Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/43/f4/01ebb9207f29e645a64699b9ce0eefeff8e7a33494e1d29bb53736f7766b/asyncpg-0.31.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e5d5098f63beeae93512ee513d4c0c53dc12e9aa2b7a1af5a81cddf93fe4e4da", size = 2851050, upload-time = "2025-11-24T23:25:43.153Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3e/f4/03ff1426acc87be0f4e8d40fa2bff5c3952bef0080062af9efc2212e3be8/asyncpg-0.31.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37fc6c00a814e18eef51833545d1891cac9aa69140598bb076b4cd29b3e010b9", size = 2962574, upload-time = "2025-11-24T23:25:44.942Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/39/cc788dfca3d4060f9d93e67be396ceec458dfc429e26139059e58c2c244d/asyncpg-0.31.0-cp311-cp311-win32.whl", hash = "sha256:5a4af56edf82a701aece93190cc4e094d2df7d33f6e915c222fb09efbb5afc24", size = 521076, upload-time = "2025-11-24T23:25:46.486Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/28/fc/735af5384c029eb7f1ca60ccb8fa95521dbdaeef788edf4cecfc604c3cab/asyncpg-0.31.0-cp311-cp311-win_amd64.whl", hash = "sha256:480c4befbdf079c14c9ca43c8c5e1fe8b6296c96f1f927158d4f1e750aacc047", size = 584980, upload-time = "2025-11-24T23:25:47.938Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/a6/59d0a146e61d20e18db7396583242e32e0f120693b67a8de43f1557033e2/asyncpg-0.31.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b44c31e1efc1c15188ef183f287c728e2046abb1d26af4d20858215d50d91fad", size = 662042, upload-time = "2025-11-24T23:25:49.578Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/01/ffaa189dcb63a2471720615e60185c3f6327716fdc0fc04334436fbb7c65/asyncpg-0.31.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0c89ccf741c067614c9b5fc7f1fc6f3b61ab05ae4aaa966e6fd6b93097c7d20d", size = 638504, upload-time = "2025-11-24T23:25:51.501Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/62/3f699ba45d8bd24c5d65392190d19656d74ff0185f42e19d0bbd973bb371/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:12b3b2e39dc5470abd5e98c8d3373e4b1d1234d9fbdedf538798b2c13c64460a", size = 3426241, upload-time = "2025-11-24T23:25:53.278Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8c/d1/a867c2150f9c6e7af6462637f613ba67f78a314b00db220cd26ff559d532/asyncpg-0.31.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:aad7a33913fb8bcb5454313377cc330fbb19a0cd5faa7272407d8a0c4257b671", size = 3520321, upload-time = "2025-11-24T23:25:54.982Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7a/1a/cce4c3f246805ecd285a3591222a2611141f1669d002163abef999b60f98/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3df118d94f46d85b2e434fd62c84cb66d5834d5a890725fe625f498e72e4d5ec", size = 3316685, upload-time = "2025-11-24T23:25:57.43Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/ae/0fc961179e78cc579e138fad6eb580448ecae64908f95b8cb8ee2f241f67/asyncpg-0.31.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bd5b6efff3c17c3202d4b37189969acf8927438a238c6257f66be3c426beba20", size = 3471858, upload-time = "2025-11-24T23:25:59.636Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/52/b2/b20e09670be031afa4cbfabd645caece7f85ec62d69c312239de568e058e/asyncpg-0.31.0-cp312-cp312-win32.whl", hash = "sha256:027eaa61361ec735926566f995d959ade4796f6a49d3bde17e5134b9964f9ba8", size = 527852, upload-time = "2025-11-24T23:26:01.084Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b5/f0/f2ed1de154e15b107dc692262395b3c17fc34eafe2a78fc2115931561730/asyncpg-0.31.0-cp312-cp312-win_amd64.whl", hash = "sha256:72d6bdcbc93d608a1158f17932de2321f68b1a967a13e014998db87a72ed3186", size = 597175, upload-time = "2025-11-24T23:26:02.564Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/11/97b5c2af72a5d0b9bc3fa30cd4b9ce22284a9a943a150fdc768763caf035/asyncpg-0.31.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c204fab1b91e08b0f47e90a75d1b3c62174dab21f670ad6c5d0f243a228f015b", size = 661111, upload-time = "2025-11-24T23:26:04.467Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1b/71/157d611c791a5e2d0423f09f027bd499935f0906e0c2a416ce712ba51ef3/asyncpg-0.31.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:54a64f91839ba59008eccf7aad2e93d6e3de688d796f35803235ea1c4898ae1e", size = 636928, upload-time = "2025-11-24T23:26:05.944Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2e/fc/9e3486fb2bbe69d4a867c0b76d68542650a7ff1574ca40e84c3111bb0c6e/asyncpg-0.31.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0e0822b1038dc7253b337b0f3f676cadc4ac31b126c5d42691c39691962e403", size = 3424067, upload-time = "2025-11-24T23:26:07.957Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/c6/8c9d076f73f07f995013c791e018a1cd5f31823c2a3187fc8581706aa00f/asyncpg-0.31.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bef056aa502ee34204c161c72ca1f3c274917596877f825968368b2c33f585f4", size = 3518156, upload-time = "2025-11-24T23:26:09.591Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/3b/60683a0baf50fbc546499cfb53132cb6835b92b529a05f6a81471ab60d0c/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0bfbcc5b7ffcd9b75ab1558f00db2ae07db9c80637ad1b2469c43df79d7a5ae2", size = 3319636, upload-time = "2025-11-24T23:26:11.168Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/dc/8487df0f69bd398a61e1792b3cba0e47477f214eff085ba0efa7eac9ce87/asyncpg-0.31.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22bc525ebbdc24d1261ecbf6f504998244d4e3be1721784b5f64664d61fbe602", size = 3472079, upload-time = "2025-11-24T23:26:13.164Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/a1/c5bbeeb8531c05c89135cb8b28575ac2fac618bcb60119ee9696c3faf71c/asyncpg-0.31.0-cp313-cp313-win32.whl", hash = "sha256:f890de5e1e4f7e14023619399a471ce4b71f5418cd67a51853b9910fdfa73696", size = 527606, upload-time = "2025-11-24T23:26:14.78Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/66/b25ccb84a246b470eb943b0107c07edcae51804912b824054b3413995a10/asyncpg-0.31.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc5f2fa9916f292e5c5c8b2ac2813763bcd7f58e130055b4ad8a0531314201ab", size = 596569, upload-time = "2025-11-24T23:26:16.189Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867, upload-time = "2025-11-24T23:26:17.631Z" },
|
{ url = "https://files.pythonhosted.org/packages/3c/36/e9450d62e84a13aea6580c83a47a437f26c7ca6fa0f0fd40b6670793ea30/asyncpg-0.31.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f6b56b91bb0ffc328c4e3ed113136cddd9deefdf5f79ab448598b9772831df44", size = 660867, upload-time = "2025-11-24T23:26:17.631Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349, upload-time = "2025-11-24T23:26:19.689Z" },
|
{ url = "https://files.pythonhosted.org/packages/82/4b/1d0a2b33b3102d210439338e1beea616a6122267c0df459ff0265cd5807a/asyncpg-0.31.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:334dec28cf20d7f5bb9e45b39546ddf247f8042a690bff9b9573d00086e69cb5", size = 638349, upload-time = "2025-11-24T23:26:19.689Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428, upload-time = "2025-11-24T23:26:21.275Z" },
|
{ url = "https://files.pythonhosted.org/packages/41/aa/e7f7ac9a7974f08eff9183e392b2d62516f90412686532d27e196c0f0eeb/asyncpg-0.31.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98cc158c53f46de7bb677fd20c417e264fc02b36d901cc2a43bd6cb0dc6dbfd2", size = 3410428, upload-time = "2025-11-24T23:26:21.275Z" },
|
||||||
@@ -105,21 +83,6 @@ version = "5.0.0"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386, upload-time = "2025-09-25T19:50:47.829Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/d4/36/3329e2518d70ad8e2e5817d5a4cac6bba05a47767ec416c7d020a965f408/bcrypt-5.0.0.tar.gz", hash = "sha256:f748f7c2d6fd375cc93d3fba7ef4a9e3a092421b8dbf34d8d4dc06be9492dfdd", size = 25386, upload-time = "2025-09-25T19:50:47.829Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/85/3e65e01985fddf25b64ca67275bb5bdb4040bd1a53b66d355c6c37c8a680/bcrypt-5.0.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f3c08197f3039bec79cee59a606d62b96b16669cff3949f21e74796b6e3cd2be", size = 481806, upload-time = "2025-09-25T19:49:05.102Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/dc/01eb79f12b177017a726cbf78330eb0eb442fae0e7b3dfd84ea2849552f3/bcrypt-5.0.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:200af71bc25f22006f4069060c88ed36f8aa4ff7f53e67ff04d2ab3f1e79a5b2", size = 268626, upload-time = "2025-09-25T19:49:06.723Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8c/cf/e82388ad5959c40d6afd94fb4743cc077129d45b952d46bdc3180310e2df/bcrypt-5.0.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:baade0a5657654c2984468efb7d6c110db87ea63ef5a4b54732e7e337253e44f", size = 271853, upload-time = "2025-09-25T19:49:08.028Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/86/7134b9dae7cf0efa85671651341f6afa695857fae172615e960fb6a466fa/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:c58b56cdfb03202b3bcc9fd8daee8e8e9b6d7e3163aa97c631dfcfcc24d36c86", size = 269793, upload-time = "2025-09-25T19:49:09.727Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cc/82/6296688ac1b9e503d034e7d0614d56e80c5d1a08402ff856a4549cb59207/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4bfd2a34de661f34d0bda43c3e4e79df586e4716ef401fe31ea39d69d581ef23", size = 289930, upload-time = "2025-09-25T19:49:11.204Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/18/884a44aa47f2a3b88dd09bc05a1e40b57878ecd111d17e5bba6f09f8bb77/bcrypt-5.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ed2e1365e31fc73f1825fa830f1c8f8917ca1b3ca6185773b349c20fd606cec2", size = 272194, upload-time = "2025-09-25T19:49:12.524Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0e/8f/371a3ab33c6982070b674f1788e05b656cfbf5685894acbfef0c65483a59/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:83e787d7a84dbbfba6f250dd7a5efd689e935f03dd83b0f919d39349e1f23f83", size = 269381, upload-time = "2025-09-25T19:49:14.308Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/34/7e4e6abb7a8778db6422e88b1f06eb07c47682313997ee8a8f9352e5a6f1/bcrypt-5.0.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:137c5156524328a24b9fac1cb5db0ba618bc97d11970b39184c1d87dc4bf1746", size = 271750, upload-time = "2025-09-25T19:49:15.584Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c0/1b/54f416be2499bd72123c70d98d36c6cd61a4e33d9b89562c22481c81bb30/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:38cac74101777a6a7d3b3e3cfefa57089b5ada650dce2baf0cbdd9d65db22a9e", size = 303757, upload-time = "2025-09-25T19:49:17.244Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/13/62/062c24c7bcf9d2826a1a843d0d605c65a755bc98002923d01fd61270705a/bcrypt-5.0.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:d8d65b564ec849643d9f7ea05c6d9f0cd7ca23bdd4ac0c2dbef1104ab504543d", size = 306740, upload-time = "2025-09-25T19:49:18.693Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/c8/1fdbfc8c0f20875b6b4020f3c7dc447b8de60aa0be5faaf009d24242aec9/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:741449132f64b3524e95cd30e5cd3343006ce146088f074f31ab26b94e6c75ba", size = 334197, upload-time = "2025-09-25T19:49:20.523Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a6/c1/8b84545382d75bef226fbc6588af0f7b7d095f7cd6a670b42a86243183cd/bcrypt-5.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:212139484ab3207b1f0c00633d3be92fef3c5f0af17cad155679d03ff2ee1e41", size = 352974, upload-time = "2025-09-25T19:49:22.254Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/a6/ffb49d4254ed085e62e3e5dd05982b4393e32fe1e49bb1130186617c29cd/bcrypt-5.0.0-cp313-cp313t-win32.whl", hash = "sha256:9d52ed507c2488eddd6a95bccee4e808d3234fa78dd370e24bac65a21212b861", size = 148498, upload-time = "2025-09-25T19:49:24.134Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/48/a9/259559edc85258b6d5fc5471a62a3299a6aa37a6611a169756bf4689323c/bcrypt-5.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:f6984a24db30548fd39a44360532898c33528b74aedf81c26cf29c51ee47057e", size = 145853, upload-time = "2025-09-25T19:49:25.702Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2d/df/9714173403c7e8b245acf8e4be8876aac64a209d1b392af457c79e60492e/bcrypt-5.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9fffdb387abe6aa775af36ef16f55e318dcda4194ddbf82007a6f21da29de8f5", size = 139626, upload-time = "2025-09-25T19:49:26.928Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862, upload-time = "2025-09-25T19:49:28.365Z" },
|
{ url = "https://files.pythonhosted.org/packages/f8/14/c18006f91816606a4abe294ccc5d1e6f0e42304df5a33710e9e8e95416e1/bcrypt-5.0.0-cp314-cp314t-macosx_10_12_universal2.whl", hash = "sha256:4870a52610537037adb382444fefd3706d96d663ac44cbb2f37e3919dca3d7ef", size = 481862, upload-time = "2025-09-25T19:49:28.365Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544, upload-time = "2025-09-25T19:49:30.39Z" },
|
{ url = "https://files.pythonhosted.org/packages/67/49/dd074d831f00e589537e07a0725cf0e220d1f0d5d8e85ad5bbff251c45aa/bcrypt-5.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:48f753100931605686f74e27a7b49238122aa761a9aefe9373265b8b7aa43ea4", size = 268544, upload-time = "2025-09-25T19:49:30.39Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787, upload-time = "2025-09-25T19:49:32.144Z" },
|
{ url = "https://files.pythonhosted.org/packages/f5/91/50ccba088b8c474545b034a1424d05195d9fcbaaf802ab8bfe2be5a4e0d7/bcrypt-5.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f70aadb7a809305226daedf75d90379c397b094755a710d7014b8b117df1ebbf", size = 271787, upload-time = "2025-09-25T19:49:32.144Z" },
|
||||||
@@ -163,10 +126,41 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725, upload-time = "2025-09-25T19:50:34.384Z" },
|
{ url = "https://files.pythonhosted.org/packages/c0/f6/688d2cd64bfd0b14d805ddb8a565e11ca1fb0fd6817175d58b10052b6d88/bcrypt-5.0.0-cp39-abi3-win32.whl", hash = "sha256:64d7ce196203e468c457c37ec22390f1a61c85c6f0b8160fd752940ccfb3a683", size = 153725, upload-time = "2025-09-25T19:50:34.384Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912, upload-time = "2025-09-25T19:50:35.69Z" },
|
{ url = "https://files.pythonhosted.org/packages/9f/b9/9d9a641194a730bda138b3dfe53f584d61c58cd5230e37566e83ec2ffa0d/bcrypt-5.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:64ee8434b0da054d830fa8e89e1c8bf30061d539044a39524ff7dec90481e5c2", size = 150912, upload-time = "2025-09-25T19:50:35.69Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" },
|
{ url = "https://files.pythonhosted.org/packages/27/44/d2ef5e87509158ad2187f4dd0852df80695bb1ee0cfe0a684727b01a69e0/bcrypt-5.0.0-cp39-abi3-win_arm64.whl", hash = "sha256:f2347d3534e76bf50bca5500989d6c1d05ed64b440408057a37673282c654927", size = 144953, upload-time = "2025-09-25T19:50:37.32Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/75/4aa9f5a4d40d762892066ba1046000b329c7cd58e888a6db878019b282dc/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7edda91d5ab52b15636d9c30da87d2cc84f426c72b9dba7a9b4fe142ba11f534", size = 271180, upload-time = "2025-09-25T19:50:38.575Z" },
|
]
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/79/875f9558179573d40a9cc743038ac2bf67dfb79cecb1e8b5d70e88c94c3d/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:046ad6db88edb3c5ece4369af997938fb1c19d6a699b9c1b27b0db432faae4c4", size = 273791, upload-time = "2025-09-25T19:50:39.913Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/fe/975adb8c216174bf70fc17535f75e85ac06ed5252ea077be10d9cff5ce24/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:dcd58e2b3a908b5ecc9b9df2f0085592506ac2d5110786018ee5e160f28e0911", size = 270746, upload-time = "2025-09-25T19:50:43.306Z" },
|
[[package]]
|
||||||
{ url = "https://files.pythonhosted.org/packages/e4/f8/972c96f5a2b6c4b3deca57009d93e946bbdbe2241dca9806d502f29dd3ee/bcrypt-5.0.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:6b8f520b61e8781efee73cba14e3e8c9556ccfb375623f4f97429544734545b4", size = 273375, upload-time = "2025-09-25T19:50:45.43Z" },
|
name = "beautifulsoup4"
|
||||||
|
version = "4.14.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "soupsieve" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c3/b0/1c6a16426d389813b48d95e26898aff79abbde42ad353958ad95cc8c9b21/beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86", size = 627737, upload-time = "2025-11-30T15:08:26.084Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/39/47f9197bdd44df24d67ac8893641e16f386c984a0619ef2ee4c51fbbc019/beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb", size = 107721, upload-time = "2025-11-30T15:08:24.087Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "black"
|
||||||
|
version = "26.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "mypy-extensions" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pathspec" },
|
||||||
|
{ name = "platformdirs" },
|
||||||
|
{ name = "pytokens" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234, upload-time = "2026-03-12T03:40:30.964Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522, upload-time = "2026-03-12T03:40:32.346Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824, upload-time = "2026-03-12T03:40:33.636Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855, upload-time = "2026-03-12T03:40:35.442Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109, upload-time = "2026-03-12T03:40:36.832Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -187,43 +181,6 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
|
{ url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
|
{ url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
|
{ url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
|
||||||
@@ -320,12 +277,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" },
|
{ url = "https://files.pythonhosted.org/packages/e9/23/cbb2036e450980f65c6e0a173b73a56ff3bccd8998965dea5cc9ddd424a5/cryptography-46.0.4-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:93d8291da8d71024379ab2cb0b5c57915300155ad42e07f76bea6ad838d7e59b", size = 4664837, upload-time = "2026-01-28T00:24:17.629Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0a/21/f7433d18fe6d5845329cbdc597e30caf983229c7a245bcf54afecc555938/cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc", size = 3009779, upload-time = "2026-01-28T00:24:20.198Z" },
|
{ url = "https://files.pythonhosted.org/packages/0a/21/f7433d18fe6d5845329cbdc597e30caf983229c7a245bcf54afecc555938/cryptography-46.0.4-cp38-abi3-win32.whl", hash = "sha256:0563655cb3c6d05fb2afe693340bc050c30f9f34e15763361cf08e94749401fc", size = 3009779, upload-time = "2026-01-28T00:24:20.198Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/3a/6a/bd2e7caa2facffedf172a45c1a02e551e6d7d4828658c9a245516a598d94/cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", size = 3466633, upload-time = "2026-01-28T00:24:21.851Z" },
|
{ url = "https://files.pythonhosted.org/packages/3a/6a/bd2e7caa2facffedf172a45c1a02e551e6d7d4828658c9a245516a598d94/cryptography-46.0.4-cp38-abi3-win_amd64.whl", hash = "sha256:fa0900b9ef9c49728887d1576fd8d9e7e3ea872fa9b25ef9b64888adc434e976", size = 3466633, upload-time = "2026-01-28T00:24:21.851Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/59/e0/f9c6c53e1f2a1c2507f00f2faba00f01d2f334b35b0fbfe5286715da2184/cryptography-46.0.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:766330cce7416c92b5e90c3bb71b1b79521760cdcfc3a6a1a182d4c9fab23d2b", size = 3476316, upload-time = "2026-01-28T00:24:24.144Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/7a/f8d2d13227a9a1a9fe9c7442b057efecffa41f1e3c51d8622f26b9edbe8f/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c236a44acfb610e70f6b3e1c3ca20ff24459659231ef2f8c48e879e2d32b73da", size = 4216693, upload-time = "2026-01-28T00:24:25.758Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c5/de/3787054e8f7972658370198753835d9d680f6cd4a39df9f877b57f0dd69c/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:8a15fb869670efa8f83cbffbc8753c1abf236883225aed74cd179b720ac9ec80", size = 4382765, upload-time = "2026-01-28T00:24:27.577Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8a/5f/60e0afb019973ba6a0b322e86b3d61edf487a4f5597618a430a2a15f2d22/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:fdc3daab53b212472f1524d070735b2f0c214239df131903bae1d598016fa822", size = 4216066, upload-time = "2026-01-28T00:24:29.056Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/81/8e/bf4a0de294f147fee66f879d9bae6f8e8d61515558e3d12785dd90eca0be/cryptography-46.0.4-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:44cc0675b27cadb71bdbb96099cca1fa051cd11d2ade09e5cd3a2edb929ed947", size = 4382025, upload-time = "2026-01-28T00:24:30.681Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/f4/9ceb90cfd6a3847069b0b0b353fd3075dc69b49defc70182d8af0c4ca390/cryptography-46.0.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:be8c01a7d5a55f9a47d1888162b76c8f49d62b234d88f0ff91a9fbebe32ffbc3", size = 3406043, upload-time = "2026-01-28T00:24:32.236Z" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -383,33 +334,6 @@ version = "3.3.1"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690, upload-time = "2026-01-23T15:31:02.076Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/8a/99/1cd3411c56a410994669062bd73dd58270c00cc074cac15f385a1fd91f8a/greenlet-3.3.1.tar.gz", hash = "sha256:41848f3230b58c08bb43dee542e74a2a2e34d3c59dc3076cec9151aeeedcae98", size = 184690, upload-time = "2026-01-23T15:31:02.076Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/e8/2e1462c8fdbe0f210feb5ac7ad2d9029af8be3bf45bd9fa39765f821642f/greenlet-3.3.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:5fd23b9bc6d37b563211c6abbb1b3cab27db385a4449af5c32e932f93017080c", size = 274974, upload-time = "2026-01-23T15:31:02.891Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/a8/530a401419a6b302af59f67aaf0b9ba1015855ea7e56c036b5928793c5bd/greenlet-3.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09f51496a0bfbaa9d74d36a52d2580d1ef5ed4fdfcff0a73730abfbbbe1403dd", size = 577175, upload-time = "2026-01-23T16:00:56.213Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8e/89/7e812bb9c05e1aaef9b597ac1d0962b9021d2c6269354966451e885c4e6b/greenlet-3.3.1-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb0feb07fe6e6a74615ee62a880007d976cf739b6669cce95daa7373d4fc69c5", size = 590401, upload-time = "2026-01-23T16:05:26.365Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/70/ae/e2d5f0e59b94a2269b68a629173263fa40b63da32f5c231307c349315871/greenlet-3.3.1-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:67ea3fc73c8cd92f42467a72b75e8f05ed51a0e9b1d15398c913416f2dafd49f", size = 601161, upload-time = "2026-01-23T16:15:53.456Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5c/ae/8d472e1f5ac5efe55c563f3eabb38c98a44b832602e12910750a7c025802/greenlet-3.3.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:39eda9ba259cc9801da05351eaa8576e9aa83eb9411e8f0c299e05d712a210f2", size = 590272, upload-time = "2026-01-23T15:32:49.411Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/51/0fde34bebfcadc833550717eade64e35ec8738e6b097d5d248274a01258b/greenlet-3.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e2e7e882f83149f0a71ac822ebf156d902e7a5d22c9045e3e0d1daf59cee2cc9", size = 1550729, upload-time = "2026-01-23T16:04:20.867Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/c9/2fb47bee83b25b119d5a35d580807bb8b92480a54b68fef009a02945629f/greenlet-3.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:80aa4d79eb5564f2e0a6144fcc744b5a37c56c4a92d60920720e99210d88db0f", size = 1615552, upload-time = "2026-01-23T15:33:45.743Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/54/dcf9f737b96606f82f8dd05becfb8d238db0633dd7397d542a296fe9cad3/greenlet-3.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:32e4ca9777c5addcbf42ff3915d99030d8e00173a56f80001fb3875998fe410b", size = 226462, upload-time = "2026-01-23T15:36:50.422Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/37/61e1015cf944ddd2337447d8e97fb423ac9bc21f9963fb5f206b53d65649/greenlet-3.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:da19609432f353fed186cc1b85e9440db93d489f198b4bdf42ae19cc9d9ac9b4", size = 225715, upload-time = "2026-01-23T15:33:17.298Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/c8/9d76a66421d1ae24340dfae7e79c313957f6e3195c144d2c73333b5bfe34/greenlet-3.3.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:7e806ca53acf6d15a888405880766ec84721aa4181261cd11a457dfe9a7a4975", size = 276443, upload-time = "2026-01-23T15:30:10.066Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/81/99/401ff34bb3c032d1f10477d199724f5e5f6fbfb59816ad1455c79c1eb8e7/greenlet-3.3.1-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d842c94b9155f1c9b3058036c24ffb8ff78b428414a19792b2380be9cecf4f36", size = 597359, upload-time = "2026-01-23T16:00:57.394Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/bc/4dcc0871ed557792d304f50be0f7487a14e017952ec689effe2180a6ff35/greenlet-3.3.1-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:20fedaadd422fa02695f82093f9a98bad3dab5fcda793c658b945fcde2ab27ba", size = 607805, upload-time = "2026-01-23T16:05:28.068Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3b/cd/7a7ca57588dac3389e97f7c9521cb6641fd8b6602faf1eaa4188384757df/greenlet-3.3.1-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c620051669fd04ac6b60ebc70478210119c56e2d5d5df848baec4312e260e4ca", size = 622363, upload-time = "2026-01-23T16:15:54.754Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cf/05/821587cf19e2ce1f2b24945d890b164401e5085f9d09cbd969b0c193cd20/greenlet-3.3.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14194f5f4305800ff329cbf02c5fcc88f01886cadd29941b807668a45f0d2336", size = 609947, upload-time = "2026-01-23T15:32:51.004Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a4/52/ee8c46ed9f8babaa93a19e577f26e3d28a519feac6350ed6f25f1afee7e9/greenlet-3.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7b2fe4150a0cf59f847a67db8c155ac36aed89080a6a639e9f16df5d6c6096f1", size = 1567487, upload-time = "2026-01-23T16:04:22.125Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8f/7c/456a74f07029597626f3a6db71b273a3632aecb9afafeeca452cfa633197/greenlet-3.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49f4ad195d45f4a66a0eb9c1ba4832bb380570d361912fa3554746830d332149", size = 1636087, upload-time = "2026-01-23T15:33:47.486Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/34/2f/5e0e41f33c69655300a5e54aeb637cf8ff57f1786a3aba374eacc0228c1d/greenlet-3.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:cc98b9c4e4870fa983436afa999d4eb16b12872fab7071423d5262fa7120d57a", size = 227156, upload-time = "2026-01-23T15:34:34.808Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c8/ab/717c58343cf02c5265b531384b248787e04d8160b8afe53d9eec053d7b44/greenlet-3.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bfb2d1763d777de5ee495c85309460f6fd8146e50ec9d0ae0183dbf6f0a829d1", size = 226403, upload-time = "2026-01-23T15:31:39.372Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ec/ab/d26750f2b7242c2b90ea2ad71de70cfcd73a948a49513188a0fc0d6fc15a/greenlet-3.3.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:7ab327905cabb0622adca5971e488064e35115430cec2c35a50fd36e72a315b3", size = 275205, upload-time = "2026-01-23T15:30:24.556Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/d3/be7d19e8fad7c5a78eeefb2d896a08cd4643e1e90c605c4be3b46264998f/greenlet-3.3.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65be2f026ca6a176f88fb935ee23c18333ccea97048076aef4db1ef5bc0713ac", size = 599284, upload-time = "2026-01-23T16:00:58.584Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/21/fe703aaa056fdb0f17e5afd4b5c80195bbdab701208918938bd15b00d39b/greenlet-3.3.1-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7a3ae05b3d225b4155bda56b072ceb09d05e974bc74be6c3fc15463cf69f33fd", size = 610274, upload-time = "2026-01-23T16:05:29.312Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/06/00/95df0b6a935103c0452dad2203f5be8377e551b8466a29650c4c5a5af6cc/greenlet-3.3.1-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:12184c61e5d64268a160226fb4818af4df02cfead8379d7f8b99a56c3a54ff3e", size = 624375, upload-time = "2026-01-23T16:15:55.915Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/86/5c6ab23bb3c28c21ed6bebad006515cfe08b04613eb105ca0041fecca852/greenlet-3.3.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6423481193bbbe871313de5fd06a082f2649e7ce6e08015d2a76c1e9186ca5b3", size = 612904, upload-time = "2026-01-23T15:32:52.317Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/f3/7949994264e22639e40718c2daf6f6df5169bf48fb038c008a489ec53a50/greenlet-3.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:33a956fe78bbbda82bfc95e128d61129b32d66bcf0a20a1f0c08aa4839ffa951", size = 1567316, upload-time = "2026-01-23T16:04:23.316Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8d/6e/d73c94d13b6465e9f7cd6231c68abde838bb22408596c05d9059830b7872/greenlet-3.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b065d3284be43728dd280f6f9a13990b56470b81be20375a207cdc814a983f2", size = 1636549, upload-time = "2026-01-23T15:33:48.643Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5e/b3/c9c23a6478b3bcc91f979ce4ca50879e4d0b2bd7b9a53d8ecded719b92e2/greenlet-3.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:27289986f4e5b0edec7b5a91063c109f0276abb09a7e9bdab08437525977c946", size = 227042, upload-time = "2026-01-23T15:33:58.216Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/90/e7/824beda656097edee36ab15809fd063447b200cc03a7f6a24c34d520bc88/greenlet-3.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:2f080e028001c5273e0b42690eaf359aeef9cb1389da0f171ea51a5dc3c7608d", size = 226294, upload-time = "2026-01-23T15:30:52.73Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737, upload-time = "2026-01-23T15:32:16.889Z" },
|
{ url = "https://files.pythonhosted.org/packages/ae/fb/011c7c717213182caf78084a9bea51c8590b0afda98001f69d9f853a495b/greenlet-3.3.1-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:bd59acd8529b372775cd0fcbc5f420ae20681c5b045ce25bd453ed8455ab99b5", size = 275737, upload-time = "2026-01-23T15:32:16.889Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422, upload-time = "2026-01-23T16:01:00.354Z" },
|
{ url = "https://files.pythonhosted.org/packages/41/2e/a3a417d620363fdbb08a48b1dd582956a46a61bf8fd27ee8164f9dfe87c2/greenlet-3.3.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b31c05dd84ef6871dd47120386aed35323c944d86c3d91a17c4b8d23df62f15b", size = 646422, upload-time = "2026-01-23T16:01:00.354Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219, upload-time = "2026-01-23T16:05:30.956Z" },
|
{ url = "https://files.pythonhosted.org/packages/b4/09/c6c4a0db47defafd2d6bab8ddfe47ad19963b4e30f5bed84d75328059f8c/greenlet-3.3.1-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02925a0bfffc41e542c70aa14c7eda3593e4d7e274bfcccca1827e6c0875902e", size = 658219, upload-time = "2026-01-23T16:05:30.956Z" },
|
||||||
@@ -457,27 +381,6 @@ version = "0.7.1"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6d/70/023d7ce117993107be88d2cbca566a7c1323ccbaf0af7eabf2064fe356f6/httptools-0.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69d4f9705c405ae3ee83d6a12283dc9feba8cc6aaec671b412917e644ab4fa66", size = 473268, upload-time = "2025-10-10T03:54:49.993Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/32/4d/9dd616c38da088e3f436e9a616e1d0cc66544b8cdac405cc4e81c8679fc7/httptools-0.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:44c8f4347d4b31269c8a9205d8a5ee2df5322b09bbbd30f8f862185bb6b05346", size = 455517, upload-time = "2025-10-10T03:54:51.066Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1d/3a/a6c595c310b7df958e739aae88724e24f9246a514d909547778d776799be/httptools-0.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:465275d76db4d554918aba40bf1cbebe324670f3dfc979eaffaa5d108e2ed650", size = 458337, upload-time = "2025-10-10T03:54:52.196Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fd/82/88e8d6d2c51edc1cc391b6e044c6c435b6aebe97b1abc33db1b0b24cd582/httptools-0.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:322d00c2068d125bd570f7bf78b2d367dad02b919d8581d7476d8b75b294e3e6", size = 85743, upload-time = "2025-10-10T03:54:53.448Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" },
|
{ url = "https://files.pythonhosted.org/packages/34/50/9d095fcbb6de2d523e027a2f304d4551855c2f46e0b82befd718b8b20056/httptools-0.7.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:c08fe65728b8d70b6923ce31e3956f859d5e1e8548e6f22ec520a962c6757270", size = 203619, upload-time = "2025-10-10T03:54:54.321Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" },
|
{ url = "https://files.pythonhosted.org/packages/07/f0/89720dc5139ae54b03f861b5e2c55a37dba9a5da7d51e1e824a1f343627f/httptools-0.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7aea2e3c3953521c3c51106ee11487a910d45586e351202474d45472db7d72d3", size = 108714, upload-time = "2025-10-10T03:54:55.163Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" },
|
{ url = "https://files.pythonhosted.org/packages/b3/cb/eea88506f191fb552c11787c23f9a405f4c7b0c5799bf73f2249cd4f5228/httptools-0.7.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0e68b8582f4ea9166be62926077a3334064d422cf08ab87d8b74664f8e9058e1", size = 472909, upload-time = "2025-10-10T03:54:56.056Z" },
|
||||||
@@ -511,17 +414,80 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mypy-extensions"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "networkx"
|
||||||
|
version = "3.6.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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 = "passlib"
|
||||||
|
version = "1.7.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b6/06/9da9ee59a67fae7761aab3ccc84fa4f3f33f125b370f1ccdb915bf967c11/passlib-1.7.4.tar.gz", hash = "sha256:defd50f72b65c5402ab2c573830a6978e5f202ad0d984793c8dde2c4152ebe04", size = 689844, upload-time = "2020-10-08T19:00:52.121Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/a4/ab6b7589382ca3df236e03faa71deac88cae040af60c071a78d254a62172/passlib-1.7.4-py2.py3-none-any.whl", hash = "sha256:aa6bca462b8d8bda89c70b382f0c298a20b5560af6cbfa2dce410c0a2fb669f1", size = 525554, upload-time = "2020-10-08T19:00:49.856Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.optional-dependencies]
|
||||||
|
bcrypt = [
|
||||||
|
{ name = "bcrypt" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathspec"
|
||||||
|
version = "1.0.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "planet"
|
name = "planet"
|
||||||
version = "1.0.0"
|
version = "1.0.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "aiofiles" },
|
{ name = "aiofiles" },
|
||||||
|
{ name = "apscheduler" },
|
||||||
{ name = "asyncpg" },
|
{ name = "asyncpg" },
|
||||||
{ name = "bcrypt" },
|
{ name = "bcrypt" },
|
||||||
|
{ name = "beautifulsoup4" },
|
||||||
{ name = "email-validator" },
|
{ name = "email-validator" },
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
|
{ name = "networkx" },
|
||||||
|
{ name = "passlib", extra = ["bcrypt"] },
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
{ name = "pydantic-settings" },
|
{ name = "pydantic-settings" },
|
||||||
{ name = "python-dotenv" },
|
{ name = "python-dotenv" },
|
||||||
@@ -532,14 +498,26 @@ dependencies = [
|
|||||||
{ name = "uvicorn", extra = ["standard"] },
|
{ name = "uvicorn", extra = ["standard"] },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "black" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
{ name = "pytest-asyncio" },
|
||||||
|
{ name = "ruff" },
|
||||||
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "aiofiles", specifier = ">=23.2.1" },
|
{ name = "aiofiles", specifier = ">=23.2.1" },
|
||||||
|
{ name = "apscheduler", specifier = ">=3.10.4" },
|
||||||
{ name = "asyncpg", specifier = ">=0.29.0" },
|
{ name = "asyncpg", specifier = ">=0.29.0" },
|
||||||
{ name = "bcrypt", specifier = ">=4.0.0" },
|
{ name = "bcrypt", specifier = ">=4.0.0" },
|
||||||
|
{ name = "beautifulsoup4", specifier = ">=4.12.0" },
|
||||||
{ name = "email-validator", specifier = ">=2.1.0" },
|
{ name = "email-validator", specifier = ">=2.1.0" },
|
||||||
{ name = "fastapi", specifier = ">=0.109.0" },
|
{ name = "fastapi", specifier = ">=0.109.0" },
|
||||||
{ name = "httpx", specifier = ">=0.26.0" },
|
{ name = "httpx", specifier = ">=0.26.0" },
|
||||||
|
{ name = "networkx", specifier = ">=3.0" },
|
||||||
|
{ name = "passlib", extras = ["bcrypt"], specifier = ">=1.7.4" },
|
||||||
{ name = "pydantic", specifier = ">=2.5.0" },
|
{ name = "pydantic", specifier = ">=2.5.0" },
|
||||||
{ name = "pydantic-settings", specifier = ">=2.1.0" },
|
{ name = "pydantic-settings", specifier = ">=2.1.0" },
|
||||||
{ name = "python-dotenv", specifier = ">=1.0.0" },
|
{ name = "python-dotenv", specifier = ">=1.0.0" },
|
||||||
@@ -550,6 +528,32 @@ requires-dist = [
|
|||||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.27.0" },
|
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.27.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [
|
||||||
|
{ name = "black", specifier = ">=24.0.0" },
|
||||||
|
{ name = "pytest", specifier = ">=7.4.0" },
|
||||||
|
{ name = "pytest-asyncio", specifier = ">=0.23.0" },
|
||||||
|
{ name = "ruff", specifier = ">=0.6.0" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "platformdirs"
|
||||||
|
version = "4.9.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyasn1"
|
name = "pyasn1"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@@ -592,48 +596,6 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
|
{ url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
|
{ url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
|
{ url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" },
|
||||||
@@ -662,22 +624,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
|
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
|
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" },
|
||||||
{ 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" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -694,6 +640,43 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" },
|
{ url = "https://files.pythonhosted.org/packages/c1/60/5d4751ba3f4a40a6891f24eec885f51afd78d208498268c734e256fb13c4/pydantic_settings-2.12.0-py3-none-any.whl", hash = "sha256:fddb9fd99a5b18da837b29710391e945b1e30c135477f484084ee513adb93809", size = 51880, upload-time = "2025-11-10T14:25:45.546Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.19.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "9.0.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest-asyncio"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-dotenv"
|
name = "python-dotenv"
|
||||||
version = "1.2.1"
|
version = "1.2.1"
|
||||||
@@ -731,41 +714,31 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" },
|
{ url = "https://files.pythonhosted.org/packages/1b/d0/397f9626e711ff749a95d96b7af99b9c566a9bb5129b8e4c10fc4d100304/python_multipart-0.0.22-py3-none-any.whl", hash = "sha256:2b2cd894c83d21bf49d702499531c7bafd057d730c201782048f7945d82de155", size = 24579, upload-time = "2026-01-25T10:15:54.811Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytokens"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pyyaml"
|
name = "pyyaml"
|
||||||
version = "6.0.3"
|
version = "6.0.3"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
||||||
@@ -790,9 +763,6 @@ wheels = [
|
|||||||
name = "redis"
|
name = "redis"
|
||||||
version = "7.1.0"
|
version = "7.1.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
|
||||||
{ name = "async-timeout", marker = "python_full_version < '3.11.3'" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/43/c8/983d5c6579a411d8a99bc5823cc5712768859b5ce2c8afe1a65b37832c81/redis-7.1.0.tar.gz", hash = "sha256:b1cc3cfa5a2cb9c2ab3ba700864fb0ad75617b41f01352ce5779dabf6d5f9c3c", size = 4796669, upload-time = "2025-11-19T15:54:39.961Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" },
|
{ url = "https://files.pythonhosted.org/packages/89/f0/8956f8a86b20d7bb9d6ac0187cf4cd54d8065bc9a1a09eb8011d4d326596/redis-7.1.0-py3-none-any.whl", hash = "sha256:23c52b208f92b56103e17c5d06bdc1a6c2c0b3106583985a76a18f83b265de2b", size = 354159, upload-time = "2025-11-19T15:54:38.064Z" },
|
||||||
@@ -810,6 +780,31 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
|
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ruff"
|
||||||
|
version = "0.15.7"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/a1/22/9e4f66ee588588dc6c9af6a994e12d26e19efbe874d1a909d09a6dac7a59/ruff-0.15.7.tar.gz", hash = "sha256:04f1ae61fc20fe0b148617c324d9d009b5f63412c0b16474f3d5f1a1a665f7ac", size = 4601277, upload-time = "2026-03-19T16:26:22.605Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/41/2f/0b08ced94412af091807b6119ca03755d651d3d93a242682bf020189db94/ruff-0.15.7-py3-none-linux_armv6l.whl", hash = "sha256:a81cc5b6910fb7dfc7c32d20652e50fa05963f6e13ead3c5915c41ac5d16668e", size = 10489037, upload-time = "2026-03-19T16:26:32.47Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/91/4a/82e0fa632e5c8b1eba5ee86ecd929e8ff327bbdbfb3c6ac5d81631bef605/ruff-0.15.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:722d165bd52403f3bdabc0ce9e41fc47070ac56d7a91b4e0d097b516a53a3477", size = 10955433, upload-time = "2026-03-19T16:27:00.205Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ab/10/12586735d0ff42526ad78c049bf51d7428618c8b5c467e72508c694119df/ruff-0.15.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7fbc2448094262552146cbe1b9643a92f66559d3761f1ad0656d4991491af49e", size = 10269302, upload-time = "2026-03-19T16:26:26.183Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/5d/32b5c44ccf149a26623671df49cbfbd0a0ae511ff3df9d9d2426966a8d57/ruff-0.15.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b39329b60eba44156d138275323cc726bbfbddcec3063da57caa8a8b1d50adf", size = 10607625, upload-time = "2026-03-19T16:27:03.263Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/f1/f0001cabe86173aaacb6eb9bb734aa0605f9a6aa6fa7d43cb49cbc4af9c9/ruff-0.15.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87768c151808505f2bfc93ae44e5f9e7c8518943e5074f76ac21558ef5627c85", size = 10324743, upload-time = "2026-03-19T16:27:09.791Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/87/b8a8f3d56b8d848008559e7c9d8bf367934d5367f6d932ba779456e2f73b/ruff-0.15.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fb0511670002c6c529ec66c0e30641c976c8963de26a113f3a30456b702468b0", size = 11138536, upload-time = "2026-03-19T16:27:06.101Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/f2/4fd0d05aab0c5934b2e1464784f85ba2eab9d54bffc53fb5430d1ed8b829/ruff-0.15.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e0d19644f801849229db8345180a71bee5407b429dd217f853ec515e968a6912", size = 11994292, upload-time = "2026-03-19T16:26:48.718Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/22/fc4483871e767e5e95d1622ad83dad5ebb830f762ed0420fde7dfa9d9b08/ruff-0.15.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4806d8e09ef5e84eb19ba833d0442f7e300b23fe3f0981cae159a248a10f0036", size = 11398981, upload-time = "2026-03-19T16:26:54.513Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b0/99/66f0343176d5eab02c3f7fcd2de7a8e0dd7a41f0d982bee56cd1c24db62b/ruff-0.15.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dce0896488562f09a27b9c91b1f58a097457143931f3c4d519690dea54e624c5", size = 11242422, upload-time = "2026-03-19T16:26:29.277Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5d/3a/a7060f145bfdcce4c987ea27788b30c60e2c81d6e9a65157ca8afe646328/ruff-0.15.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1852ce241d2bc89e5dc823e03cff4ce73d816b5c6cdadd27dbfe7b03217d2a12", size = 11232158, upload-time = "2026-03-19T16:26:42.321Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/53/90fbb9e08b29c048c403558d3cdd0adf2668b02ce9d50602452e187cd4af/ruff-0.15.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5f3e4b221fb4bd293f79912fc5e93a9063ebd6d0dcbd528f91b89172a9b8436c", size = 10577861, upload-time = "2026-03-19T16:26:57.459Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/aa/5f486226538fe4d0f0439e2da1716e1acf895e2a232b26f2459c55f8ddad/ruff-0.15.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b15e48602c9c1d9bdc504b472e90b90c97dc7d46c7028011ae67f3861ceba7b4", size = 10327310, upload-time = "2026-03-19T16:26:35.909Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/9e/271afdffb81fe7bfc8c43ba079e9d96238f674380099457a74ccb3863857/ruff-0.15.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1b4705e0e85cedc74b0a23cf6a179dbb3df184cb227761979cc76c0440b5ab0d", size = 10840752, upload-time = "2026-03-19T16:26:45.723Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/29/a4ae78394f76c7759953c47884eb44de271b03a66634148d9f7d11e721bd/ruff-0.15.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:112c1fa316a558bb34319282c1200a8bf0495f1b735aeb78bfcb2991e6087580", size = 11336961, upload-time = "2026-03-19T16:26:39.076Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/26/6b/8786ba5736562220d588a2f6653e6c17e90c59ced34a2d7b512ef8956103/ruff-0.15.7-py3-none-win32.whl", hash = "sha256:6d39e2d3505b082323352f733599f28169d12e891f7dd407f2d4f54b4c2886de", size = 10582538, upload-time = "2026-03-19T16:26:15.992Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/e9/346d4d3fffc6871125e877dae8d9a1966b254fbd92a50f8561078b88b099/ruff-0.15.7-py3-none-win_amd64.whl", hash = "sha256:4d53d712ddebcd7dace1bc395367aec12c057aacfe9adbb6d832302575f4d3a1", size = 11755839, upload-time = "2026-03-19T16:26:19.897Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/e8/726643a3ea68c727da31570bde48c7a10f1aa60eddd628d94078fec586ff/ruff-0.15.7-py3-none-win_arm64.whl", hash = "sha256:18e8d73f1c3fdf27931497972250340f92e8c861722161a9caeb89a58ead6ed2", size = 11023304, upload-time = "2026-03-19T16:26:51.669Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "six"
|
name = "six"
|
||||||
version = "1.17.0"
|
version = "1.17.0"
|
||||||
@@ -819,6 +814,15 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "soupsieve"
|
||||||
|
version = "2.8.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7b/ae/2d9c981590ed9999a0d91755b47fc74f74de286b0f5cee14c9269041e6c4/soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349", size = 118627, upload-time = "2026-01-20T04:27:02.457Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/2c/1462b1d0a634697ae9e55b3cecdcb64788e8b7d63f54d923fcd0bb140aed/soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95", size = 37016, upload-time = "2026-01-20T04:27:01.012Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sqlalchemy"
|
name = "sqlalchemy"
|
||||||
version = "2.0.46"
|
version = "2.0.46"
|
||||||
@@ -829,31 +833,6 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393, upload-time = "2026-01-21T18:03:45.119Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/06/aa/9ce0f3e7a9829ead5c8ce549392f33a12c4555a6c0609bb27d882e9c7ddf/sqlalchemy-2.0.46.tar.gz", hash = "sha256:cf36851ee7219c170bb0793dbc3da3e80c582e04a5437bc601bfe8c85c9216d7", size = 9865393, upload-time = "2026-01-21T18:03:45.119Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/69/ac/b42ad16800d0885105b59380ad69aad0cce5a65276e269ce2729a2343b6a/sqlalchemy-2.0.46-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:261c4b1f101b4a411154f1da2b76497d73abbfc42740029205d4d01fa1052684", size = 2154851, upload-time = "2026-01-21T18:27:30.54Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a0/60/d8710068cb79f64d002ebed62a7263c00c8fd95f4ebd4b5be8f7ca93f2bc/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:181903fe8c1b9082995325f1b2e84ac078b1189e2819380c2303a5f90e114a62", size = 3311241, upload-time = "2026-01-21T18:32:33.45Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/0f/20c71487c7219ab3aa7421c7c62d93824c97c1460f2e8bb72404b0192d13/sqlalchemy-2.0.46-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:590be24e20e2424a4c3c1b0835e9405fa3d0af5823a1a9fc02e5dff56471515f", size = 3310741, upload-time = "2026-01-21T18:44:57.887Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/65/80/d26d00b3b249ae000eee4db206fcfc564bf6ca5030e4747adf451f4b5108/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7568fe771f974abadce52669ef3a03150ff03186d8eb82613bc8adc435a03f01", size = 3263116, upload-time = "2026-01-21T18:32:35.044Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/ee/74dda7506640923821340541e8e45bd3edd8df78664f1f2e0aae8077192b/sqlalchemy-2.0.46-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf7e1e78af38047e08836d33502c7a278915698b7c2145d045f780201679999", size = 3285327, upload-time = "2026-01-21T18:44:59.254Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/25/6dcf8abafff1389a21c7185364de145107b7394ecdcb05233815b236330d/sqlalchemy-2.0.46-cp311-cp311-win32.whl", hash = "sha256:9d80ea2ac519c364a7286e8d765d6cd08648f5b21ca855a8017d9871f075542d", size = 2114564, upload-time = "2026-01-21T18:33:15.85Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/93/5f/e081490f8523adc0088f777e4ebad3cac21e498ec8a3d4067074e21447a1/sqlalchemy-2.0.46-cp311-cp311-win_amd64.whl", hash = "sha256:585af6afe518732d9ccd3aea33af2edaae4a7aa881af5d8f6f4fe3a368699597", size = 2139233, upload-time = "2026-01-21T18:33:17.528Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b6/35/d16bfa235c8b7caba3730bba43e20b1e376d2224f407c178fbf59559f23e/sqlalchemy-2.0.46-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a9a72b0da8387f15d5810f1facca8f879de9b85af8c645138cba61ea147968c", size = 2153405, upload-time = "2026-01-21T19:05:54.143Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/06/6c/3192e24486749862f495ddc6584ed730c0c994a67550ec395d872a2ad650/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2347c3f0efc4de367ba00218e0ae5c4ba2306e47216ef80d6e31761ac97cb0b9", size = 3334702, upload-time = "2026-01-21T18:46:45.384Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ea/a2/b9f33c8d68a3747d972a0bb758c6b63691f8fb8a49014bc3379ba15d4274/sqlalchemy-2.0.46-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9094c8b3197db12aa6f05c51c05daaad0a92b8c9af5388569847b03b1007fb1b", size = 3347664, upload-time = "2026-01-21T18:40:09.979Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/d2/3e59e2a91eaec9db7e8dc6b37b91489b5caeb054f670f32c95bcba98940f/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37fee2164cf21417478b6a906adc1a91d69ae9aba8f9533e67ce882f4bb1de53", size = 3277372, upload-time = "2026-01-21T18:46:47.168Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dd/dd/67bc2e368b524e2192c3927b423798deda72c003e73a1e94c21e74b20a85/sqlalchemy-2.0.46-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b1e14b2f6965a685c7128bd315e27387205429c2e339eeec55cb75ca4ab0ea2e", size = 3312425, upload-time = "2026-01-21T18:40:11.548Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/43/82/0ecd68e172bfe62247e96cb47867c2d68752566811a4e8c9d8f6e7c38a65/sqlalchemy-2.0.46-cp312-cp312-win32.whl", hash = "sha256:412f26bb4ba942d52016edc8d12fb15d91d3cd46b0047ba46e424213ad407bcb", size = 2113155, upload-time = "2026-01-21T18:42:49.748Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/2a/2821a45742073fc0331dc132552b30de68ba9563230853437cac54b2b53e/sqlalchemy-2.0.46-cp312-cp312-win_amd64.whl", hash = "sha256:ea3cd46b6713a10216323cda3333514944e510aa691c945334713fca6b5279ff", size = 2140078, upload-time = "2026-01-21T18:42:51.197Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b3/4b/fa7838fe20bb752810feed60e45625a9a8b0102c0c09971e2d1d95362992/sqlalchemy-2.0.46-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:93a12da97cca70cea10d4b4fc602589c4511f96c1f8f6c11817620c021d21d00", size = 2150268, upload-time = "2026-01-21T19:05:56.621Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/46/c1/b34dccd712e8ea846edf396e00973dda82d598cb93762e55e43e6835eba9/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af865c18752d416798dae13f83f38927c52f085c52e2f32b8ab0fef46fdd02c2", size = 3276511, upload-time = "2026-01-21T18:46:49.022Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/96/48/a04d9c94753e5d5d096c628c82a98c4793b9c08ca0e7155c3eb7d7db9f24/sqlalchemy-2.0.46-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8d679b5f318423eacb61f933a9a0f75535bfca7056daeadbf6bd5bcee6183aee", size = 3292881, upload-time = "2026-01-21T18:40:13.089Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/be/f4/06eda6e91476f90a7d8058f74311cb65a2fb68d988171aced81707189131/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64901e08c33462acc9ec3bad27fc7a5c2b6491665f2aa57564e57a4f5d7c52ad", size = 3224559, upload-time = "2026-01-21T18:46:50.974Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ab/a2/d2af04095412ca6345ac22b33b89fe8d6f32a481e613ffcb2377d931d8d0/sqlalchemy-2.0.46-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e8ac45e8f4eaac0f9f8043ea0e224158855c6a4329fd4ee37c45c61e3beb518e", size = 3262728, upload-time = "2026-01-21T18:40:14.883Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/31/48/1980c7caa5978a3b8225b4d230e69a2a6538a3562b8b31cea679b6933c83/sqlalchemy-2.0.46-cp313-cp313-win32.whl", hash = "sha256:8d3b44b3d0ab2f1319d71d9863d76eeb46766f8cf9e921ac293511804d39813f", size = 2111295, upload-time = "2026-01-21T18:42:52.366Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2d/54/f8d65bbde3d877617c4720f3c9f60e99bb7266df0d5d78b6e25e7c149f35/sqlalchemy-2.0.46-cp313-cp313-win_amd64.whl", hash = "sha256:77f8071d8fbcbb2dd11b7fd40dedd04e8ebe2eb80497916efedba844298065ef", size = 2137076, upload-time = "2026-01-21T18:42:53.924Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/56/ba/9be4f97c7eb2b9d5544f2624adfc2853e796ed51d2bb8aec90bc94b7137e/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1e8cc6cc01da346dc92d9509a63033b9b1bda4fed7a7a7807ed385c7dccdc10", size = 3556533, upload-time = "2026-01-21T18:33:06.636Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/20/a6/b1fc6634564dbb4415b7ed6419cdfeaadefd2c39cdab1e3aa07a5f2474c2/sqlalchemy-2.0.46-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96c7cca1a4babaaf3bfff3e4e606e38578856917e52f0384635a95b226c87764", size = 3523208, upload-time = "2026-01-21T18:45:08.436Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a1/d8/41e0bdfc0f930ff236f86fccd12962d8fa03713f17ed57332d38af6a3782/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2a9f9aee38039cf4755891a1e50e1effcc42ea6ba053743f452c372c3152b1b", size = 3464292, upload-time = "2026-01-21T18:33:08.208Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f0/8b/9dcbec62d95bea85f5ecad9b8d65b78cc30fb0ffceeb3597961f3712549b/sqlalchemy-2.0.46-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db23b1bf8cfe1f7fda19018e7207b20cdb5168f83c437ff7e95d19e39289c447", size = 3473497, upload-time = "2026-01-21T18:45:10.552Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e9/f8/5ecdfc73383ec496de038ed1614de9e740a82db9ad67e6e4514ebc0708a3/sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada", size = 2152079, upload-time = "2026-01-21T19:05:58.477Z" },
|
{ url = "https://files.pythonhosted.org/packages/e9/f8/5ecdfc73383ec496de038ed1614de9e740a82db9ad67e6e4514ebc0708a3/sqlalchemy-2.0.46-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:56bdd261bfd0895452006d5316cbf35739c53b9bb71a170a331fa0ea560b2ada", size = 2152079, upload-time = "2026-01-21T19:05:58.477Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e5/bf/eba3036be7663ce4d9c050bc3d63794dc29fbe01691f2bf5ccb64e048d20/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366", size = 3272216, upload-time = "2026-01-21T18:46:52.634Z" },
|
{ url = "https://files.pythonhosted.org/packages/e5/bf/eba3036be7663ce4d9c050bc3d63794dc29fbe01691f2bf5ccb64e048d20/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33e462154edb9493f6c3ad2125931e273bbd0be8ae53f3ecd1c161ea9a1dd366", size = 3272216, upload-time = "2026-01-21T18:46:52.634Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/05/45/1256fb597bb83b58a01ddb600c59fe6fdf0e5afe333f0456ed75c0f8d7bd/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d", size = 3277208, upload-time = "2026-01-21T18:40:16.38Z" },
|
{ url = "https://files.pythonhosted.org/packages/05/45/1256fb597bb83b58a01ddb600c59fe6fdf0e5afe333f0456ed75c0f8d7bd/sqlalchemy-2.0.46-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bcdce05f056622a632f1d44bb47dbdb677f58cad393612280406ce37530eb6d", size = 3277208, upload-time = "2026-01-21T18:40:16.38Z" },
|
||||||
@@ -879,7 +858,6 @@ version = "0.50.0"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "anyio" },
|
{ name = "anyio" },
|
||||||
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ba/b8/73a0e6a6e079a9d9cfa64113d771e421640b6f679a52eeb9b32f72d871a1/starlette-0.50.0.tar.gz", hash = "sha256:a2a17b22203254bcbc2e1f926d2d55f3f9497f769416b3190768befe598fa3ca", size = 2646985, upload-time = "2025-11-01T15:25:27.516Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
@@ -907,6 +885,27 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzdata"
|
||||||
|
version = "2025.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772, upload-time = "2025-12-13T17:45:35.667Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzlocal"
|
||||||
|
version = "5.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "tzdata", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uvicorn"
|
name = "uvicorn"
|
||||||
version = "0.40.0"
|
version = "0.40.0"
|
||||||
@@ -937,24 +936,6 @@ version = "0.22.1"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" },
|
{ url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" },
|
{ url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" },
|
{ url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" },
|
||||||
@@ -978,55 +959,6 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e3/1f/d66bc15ea0b728df3ed96a539c777acfcad0eb78555ad9efcaa1274688f0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f27db948078f3823a6bb3b465180db8ebecf26dd5dae6f6180bd87383b6b4428", size = 459405, upload-time = "2025-10-14T15:05:04.942Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/be/90/9f4a65c0aec3ccf032703e6db02d89a157462fbb2cf20dd415128251cac0/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:059098c3a429f62fc98e8ec62b982230ef2c8df68c79e826e37b895bc359a9c0", size = 488976, upload-time = "2025-10-14T15:05:05.905Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/37/57/ee347af605d867f712be7029bb94c8c071732a4b44792e3176fa3c612d39/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfb5862016acc9b869bb57284e6cb35fdf8e22fe59f7548858e2f971d045f150", size = 595506, upload-time = "2025-10-14T15:05:06.906Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/78/cc5ab0b86c122047f75e8fc471c67a04dee395daf847d3e59381996c8707/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:319b27255aacd9923b8a276bb14d21a5f7ff82564c744235fc5eae58d95422ae", size = 474936, upload-time = "2025-10-14T15:05:07.906Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/62/da/def65b170a3815af7bd40a3e7010bf6ab53089ef1b75d05dd5385b87cf08/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c755367e51db90e75b19454b680903631d41f9e3607fbd941d296a020c2d752d", size = 456147, upload-time = "2025-10-14T15:05:09.138Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/57/99/da6573ba71166e82d288d4df0839128004c67d2778d3b566c138695f5c0b/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c22c776292a23bfc7237a98f791b9ad3144b02116ff10d820829ce62dff46d0b", size = 630007, upload-time = "2025-10-14T15:05:10.117Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a8/51/7439c4dd39511368849eb1e53279cd3454b4a4dbace80bab88feeb83c6b5/watchfiles-1.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3a476189be23c3686bc2f4321dd501cb329c0a0469e77b7b534ee10129ae6374", size = 622280, upload-time = "2025-10-14T15:05:11.146Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/95/9c/8ed97d4bba5db6fdcdb2b298d3898f2dd5c20f6b73aee04eabe56c59677e/watchfiles-1.1.1-cp313-cp313-win32.whl", hash = "sha256:bf0a91bfb5574a2f7fc223cf95eeea79abfefa404bf1ea5e339c0c1560ae99a0", size = 272056, upload-time = "2025-10-14T15:05:12.156Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/1f/f3/c14e28429f744a260d8ceae18bf58c1d5fa56b50d006a7a9f80e1882cb0d/watchfiles-1.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:52e06553899e11e8074503c8e716d574adeeb7e68913115c4b3653c53f9bae42", size = 288162, upload-time = "2025-10-14T15:05:13.208Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/dc/61/fe0e56c40d5cd29523e398d31153218718c5786b5e636d9ae8ae79453d27/watchfiles-1.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac3cc5759570cd02662b15fbcd9d917f7ecd47efe0d6b40474eafd246f91ea18", size = 277909, upload-time = "2025-10-14T15:05:14.49Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/42/e0a7d749626f1e28c7108a99fb9bf524b501bbbeb9b261ceecde644d5a07/watchfiles-1.1.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:563b116874a9a7ce6f96f87cd0b94f7faf92d08d0021e837796f0a14318ef8da", size = 403389, upload-time = "2025-10-14T15:05:15.777Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/15/49/08732f90ce0fbbc13913f9f215c689cfc9ced345fb1bcd8829a50007cc8d/watchfiles-1.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3ad9fe1dae4ab4212d8c91e80b832425e24f421703b5a42ef2e4a1e215aff051", size = 389964, upload-time = "2025-10-14T15:05:16.85Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/27/0d/7c315d4bd5f2538910491a0393c56bf70d333d51bc5b34bee8e68e8cea19/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce70f96a46b894b36eba678f153f052967a0d06d5b5a19b336ab0dbbd029f73e", size = 448114, upload-time = "2025-10-14T15:05:17.876Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/24/9e096de47a4d11bc4df41e9d1e61776393eac4cb6eb11b3e23315b78b2cc/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cb467c999c2eff23a6417e58d75e5828716f42ed8289fe6b77a7e5a91036ca70", size = 460264, upload-time = "2025-10-14T15:05:18.962Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cc/0f/e8dea6375f1d3ba5fcb0b3583e2b493e77379834c74fd5a22d66d85d6540/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:836398932192dae4146c8f6f737d74baeac8b70ce14831a239bdb1ca882fc261", size = 487877, upload-time = "2025-10-14T15:05:20.094Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ac/5b/df24cfc6424a12deb41503b64d42fbea6b8cb357ec62ca84a5a3476f654a/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:743185e7372b7bc7c389e1badcc606931a827112fbbd37f14c537320fca08620", size = 595176, upload-time = "2025-10-14T15:05:21.134Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8f/b5/853b6757f7347de4e9b37e8cc3289283fb983cba1ab4d2d7144694871d9c/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afaeff7696e0ad9f02cbb8f56365ff4686ab205fcf9c4c5b6fdfaaa16549dd04", size = 473577, upload-time = "2025-10-14T15:05:22.306Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e1/f7/0a4467be0a56e80447c8529c9fce5b38eab4f513cb3d9bf82e7392a5696b/watchfiles-1.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f7eb7da0eb23aa2ba036d4f616d46906013a68caf61b7fdbe42fc8b25132e77", size = 455425, upload-time = "2025-10-14T15:05:23.348Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8e/e0/82583485ea00137ddf69bc84a2db88bd92ab4a6e3c405e5fb878ead8d0e7/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:831a62658609f0e5c64178211c942ace999517f5770fe9436be4c2faeba0c0ef", size = 628826, upload-time = "2025-10-14T15:05:24.398Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/28/9a/a785356fccf9fae84c0cc90570f11702ae9571036fb25932f1242c82191c/watchfiles-1.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f9a2ae5c91cecc9edd47e041a930490c31c3afb1f5e6d71de3dc671bfaca02bf", size = 622208, upload-time = "2025-10-14T15:05:25.45Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" },
|
{ url = "https://files.pythonhosted.org/packages/c3/f4/0872229324ef69b2c3edec35e84bd57a1289e7d3fe74588048ed8947a323/watchfiles-1.1.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:d1715143123baeeaeadec0528bb7441103979a1d5f6fd0e1f915383fea7ea6d5", size = 404315, upload-time = "2025-10-14T15:05:26.501Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" },
|
{ url = "https://files.pythonhosted.org/packages/7b/22/16d5331eaed1cb107b873f6ae1b69e9ced582fcf0c59a50cd84f403b1c32/watchfiles-1.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:39574d6370c4579d7f5d0ad940ce5b20db0e4117444e39b6d8f99db5676c52fd", size = 390869, upload-time = "2025-10-14T15:05:27.649Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" },
|
{ url = "https://files.pythonhosted.org/packages/b2/7e/5643bfff5acb6539b18483128fdc0ef2cccc94a5b8fbda130c823e8ed636/watchfiles-1.1.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7365b92c2e69ee952902e8f70f3ba6360d0d596d9299d55d7d386df84b6941fb", size = 449919, upload-time = "2025-10-14T15:05:28.701Z" },
|
||||||
@@ -1050,10 +982,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" },
|
{ url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" },
|
{ url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" },
|
{ url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1062,33 +990,6 @@ version = "16.0"
|
|||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" },
|
{ url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" },
|
{ url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" },
|
{ url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" },
|
||||||
@@ -1107,10 +1008,5 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" },
|
{ url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" },
|
{ url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" },
|
{ url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" },
|
||||||
{ url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
|
{ url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user