diff --git a/PixivServer/app.py b/PixivServer/app.py index 2eceda5..3b18382 100644 --- a/PixivServer/app.py +++ b/PixivServer/app.py @@ -1,9 +1,19 @@ -from contextlib import asynccontextmanager -import traceback -from PixivServer.utils import get_version -from fastapi import Depends, FastAPI, Response +import asyncio +import base64 +import contextlib +import json import logging +import os import time +import traceback +import urllib.error +import urllib.request +from contextlib import asynccontextmanager +from pathlib import Path +from urllib.parse import quote, urlparse + +import psutil +from fastapi import Depends, FastAPI, Request, Response import PixivServer import PixivServer.auth @@ -12,26 +22,151 @@ import PixivServer.routers.download_queue import PixivServer.routers.health import PixivServer.routers.metadata_queue +import PixivServer.routers.metrics import PixivServer.routers.server + # import PixivServer.routers.subscription import PixivServer.service import PixivServer.service.pixiv +from PixivServer.config.pixivutil import config as pixivutil_config +from PixivServer.config.rabbitmq import config as rabbitmq_config +from PixivServer.metrics import ( + DB_ARTWORKS, + DB_MEMBERS, + DB_PAGES, + DB_SERIES, + DB_TAGS, + DISK_DATABASE_BYTES, + DISK_DOWNLOADS_BYTES, + HTTP_REQUEST_DURATION, + HTTP_REQUEST_SIZE, + HTTP_REQUESTS_TOTAL, + HTTP_RESPONSE_SIZE, + QUEUE_DEPTH, + SERVER_INFO, + SYS_CPU_PERCENT, + SYS_DISK_TOTAL_BYTES, + SYS_DISK_USED_BYTES, + SYS_MEM_TOTAL_BYTES, + SYS_MEM_USED_BYTES, +) +from PixivServer.repository.pixivutil import PixivUtilRepository +from PixivServer.utils import get_version logger = logging.getLogger('uvicorn.pixivutil') +_SYSTEM_COLLECT_INTERVAL = 15 # seconds +_DB_STAT_COLLECT_INTERVAL = 60 # seconds +_DISK_COLLECT_INTERVAL = 300 # seconds (directory walk may be slow on large collections) +_QUEUE_COLLECT_INTERVAL = 15 # seconds + + +def _collect_system_metrics() -> None: + cpu = psutil.cpu_percent(interval=1) + vm = psutil.virtual_memory() + disk = psutil.disk_usage("/") + SYS_CPU_PERCENT.set(cpu) + SYS_MEM_USED_BYTES.set(vm.used) + SYS_MEM_TOTAL_BYTES.set(vm.total) + SYS_DISK_USED_BYTES.set(disk.used) + SYS_DISK_TOTAL_BYTES.set(disk.total) + + +def _collect_db_stats() -> None: + repo = PixivUtilRepository() + repo.open() + try: + DB_MEMBERS.set(repo.count_members()) + DB_ARTWORKS.set(repo.count_artworks()) + DB_PAGES.set(repo.count_pages()) + DB_TAGS.set(repo.count_tags()) + DB_SERIES.set(repo.count_series()) + finally: + repo.close() + + +def _collect_disk_metrics() -> None: + # Database file + WAL/SHM sidecars + db_path = pixivutil_config.db_path + db_bytes = 0 + for suffix in ("", "-wal", "-shm"): + p = db_path + suffix + if os.path.isfile(p): + db_bytes += os.path.getsize(p) + DISK_DATABASE_BYTES.set(db_bytes) + + # Downloads directory — recursive file size sum + downloads = Path(PixivServer.service.pixiv.service.downloads_folder) + total = 0 + if downloads.is_dir(): + for f in downloads.rglob("*"): + if f.is_file(): + with contextlib.suppress(OSError): + total += f.stat().st_size + DISK_DOWNLOADS_BYTES.set(total) + + +def _collect_queue_depth() -> None: + parsed = urlparse(rabbitmq_config.broker_url) + user = parsed.username or "guest" + password = parsed.password or "guest" + host = parsed.hostname or "rabbitmq" + raw_vhost = parsed.path.lstrip("/") + vhost = raw_vhost if raw_vhost else "/" + encoded_vhost = quote(vhost, safe="") + url = f"http://{host}:15672/api/queues/{encoded_vhost}/pixivutil-queue" + credentials = base64.b64encode(f"{user}:{password}".encode()).decode() + req = urllib.request.Request(url, headers={"Authorization": f"Basic {credentials}"}) + try: + with urllib.request.urlopen(req, timeout=5) as resp: + data = json.loads(resp.read()) + QUEUE_DEPTH.set(data.get("messages", 0)) + except (urllib.error.URLError, OSError, ValueError): + pass # Management API unavailable; leave metric stale + + +async def _periodic_metrics_collector() -> None: + last_system = 0.0 + last_db = 0.0 + last_disk = 0.0 + last_queue = 0.0 + while True: + now = time.monotonic() + try: + if now - last_system >= _SYSTEM_COLLECT_INTERVAL: + await asyncio.to_thread(_collect_system_metrics) + last_system = time.monotonic() + if now - last_db >= _DB_STAT_COLLECT_INTERVAL: + await asyncio.to_thread(_collect_db_stats) + last_db = time.monotonic() + if now - last_disk >= _DISK_COLLECT_INTERVAL: + await asyncio.to_thread(_collect_disk_metrics) + last_disk = time.monotonic() + if now - last_queue >= _QUEUE_COLLECT_INTERVAL: + await asyncio.to_thread(_collect_queue_depth) + last_queue = time.monotonic() + except Exception: # noqa: BLE001 + logger.warning(f"Metrics collector error: {traceback.format_exc()}") + await asyncio.sleep(1) + + @asynccontextmanager async def lifespan(_: FastAPI): try: logger.info("Setting up server.") # startup actions - time.sleep(5) + await asyncio.sleep(5) PixivServer.service.pixiv.service.open(validate_pixiv_login=False) + SERVER_INFO.info({"version": get_version()}) # PixivServer.service.subscription_service.open() except Exception as e: print(f"Encountered exception during application setup: {traceback.format_exc()}") raise e + collector_task = asyncio.create_task(_periodic_metrics_collector()) yield # shutdown actions + collector_task.cancel() + await asyncio.gather(collector_task, return_exceptions=True) PixivServer.service.pixiv.service.close() # PixivServer.service.subscription_service.close() @@ -42,10 +177,37 @@ async def lifespan(_: FastAPI): auth_dependency = [Depends(PixivServer.auth.is_valid_api_key_header)] + +@app.middleware("http") +async def request_metrics_middleware(request: Request, call_next): + start = time.perf_counter() + response = await call_next(request) + duration = time.perf_counter() - start + + route = request.scope.get("route") + endpoint = route.path if route and hasattr(route, "path") else None + if endpoint is None: + return response + status_class = f"{response.status_code // 100}xx" + req_size = int(request.headers.get("content-length", 0)) + resp_size = int(response.headers.get("content-length", 0)) + + HTTP_REQUESTS_TOTAL.labels(request.method, endpoint, status_class).inc() + HTTP_REQUEST_DURATION.labels(request.method, endpoint).observe(duration) + HTTP_REQUEST_SIZE.labels(request.method, endpoint).observe(req_size) + HTTP_RESPONSE_SIZE.labels(request.method, endpoint).observe(resp_size) + return response + + app.include_router( PixivServer.routers.health.router, prefix="/api/health", ) +app.include_router( + PixivServer.routers.metrics.router, + prefix="/metrics", + dependencies=auth_dependency, +) app.include_router( PixivServer.routers.metadata_queue.router, prefix="/api/queue/metadata", @@ -77,6 +239,7 @@ async def lifespan(_: FastAPI): # prefix="/api/subscription" # ) + @app.get("/") async def info(): return Response(content=f"PixivUtil Server {get_version()}", status_code=200) diff --git a/PixivServer/config/pixivutil.py b/PixivServer/config/pixivutil.py index 05515cd..a2996a7 100644 --- a/PixivServer/config/pixivutil.py +++ b/PixivServer/config/pixivutil.py @@ -1,8 +1,9 @@ import os + class PixivUtilConfig: def __init__(self): self.db_path: str = "./.pixivUtil2/db/db.sqlite" self.cookie: str = os.getenv("PIXIVUTIL_COOKIE") -config = PixivUtilConfig() \ No newline at end of file +config = PixivUtilConfig() diff --git a/PixivServer/config/rabbitmq.py b/PixivServer/config/rabbitmq.py index b333954..c86d3e9 100644 --- a/PixivServer/config/rabbitmq.py +++ b/PixivServer/config/rabbitmq.py @@ -1,5 +1,6 @@ import os + class RabbitConfig: def __init__(self): diff --git a/PixivServer/metrics.py b/PixivServer/metrics.py new file mode 100644 index 0000000..2d2aa6c --- /dev/null +++ b/PixivServer/metrics.py @@ -0,0 +1,49 @@ +from prometheus_client import Counter, Gauge, Histogram, Info + +# --- Server info --- +SERVER_INFO = Info("pixivutil_server", "PixivUtil Server build info") + +# --- DB stat metrics (periodic) --- +DB_MEMBERS = Gauge("pixivutil_db_members_total", "Members in pixiv_master_member") +DB_ARTWORKS = Gauge("pixivutil_db_artworks_total", "Artworks in pixiv_master_image") +DB_PAGES = Gauge("pixivutil_db_pages_total", "Pages in pixiv_manga_image") +DB_TAGS = Gauge("pixivutil_db_tags_total", "Tags in pixiv_master_tag") +DB_SERIES = Gauge("pixivutil_db_series_total", "Series in pixiv_master_series") + +# --- Disk metrics (periodic) --- +DISK_DOWNLOADS_BYTES = Gauge("pixivutil_disk_downloads_bytes", "Bytes used by downloads directory") +DISK_DATABASE_BYTES = Gauge("pixivutil_disk_database_bytes", "Bytes used by SQLite database file(s)") + +# --- OS system metrics (periodic) --- +SYS_CPU_PERCENT = Gauge("pixivutil_cpu_usage_percent", "CPU usage percent") +SYS_MEM_USED_BYTES = Gauge("pixivutil_memory_used_bytes", "Memory used bytes") +SYS_MEM_TOTAL_BYTES = Gauge("pixivutil_memory_total_bytes", "Memory total bytes") +SYS_DISK_USED_BYTES = Gauge("pixivutil_sys_disk_used_bytes", "Host disk used bytes (root filesystem)") +SYS_DISK_TOTAL_BYTES = Gauge("pixivutil_sys_disk_total_bytes", "Host disk total bytes (root filesystem)") + +# --- Worker queue metrics (periodic) --- +QUEUE_DEPTH = Gauge("pixivutil_queue_depth", "Number of messages pending in the task queue") + +# --- Request metrics (per-request via middleware) --- +HTTP_REQUESTS_TOTAL = Counter( + "pixivutil_http_requests_total", + "Total HTTP requests", + ["method", "endpoint", "status_class"], +) +HTTP_REQUEST_DURATION = Histogram( + "pixivutil_http_request_duration_seconds", + "HTTP request latency", + ["method", "endpoint"], +) +HTTP_REQUEST_SIZE = Histogram( + "pixivutil_http_request_size_bytes", + "HTTP request body size in bytes", + ["method", "endpoint"], + buckets=[64, 256, 1_024, 4_096, 16_384], +) +HTTP_RESPONSE_SIZE = Histogram( + "pixivutil_http_response_size_bytes", + "HTTP response body size in bytes", + ["method", "endpoint"], + buckets=[256, 1_024, 16_384, 65_536, 1_048_576], +) diff --git a/PixivServer/models/pixiv_worker.py b/PixivServer/models/pixiv_worker.py index 8d7da48..4165827 100644 --- a/PixivServer/models/pixiv_worker.py +++ b/PixivServer/models/pixiv_worker.py @@ -2,8 +2,12 @@ Model layer for PixivUtil worker queue processing interface. """ +from pixivutil_server_common.models import ( + TagMetadataFilterMode, + TagSortOrder, + TagTypeMode, +) from pydantic import BaseModel -from pixivutil_server_common.models import TagMetadataFilterMode, TagSortOrder, TagTypeMode class DownloadArtworkByIdRequest(BaseModel): diff --git a/PixivServer/repository/pixivutil.py b/PixivServer/repository/pixivutil.py index cebc0f1..5f62715 100644 --- a/PixivServer/repository/pixivutil.py +++ b/PixivServer/repository/pixivutil.py @@ -3,19 +3,19 @@ from PixivServer.config.pixivutil import config as pixivutil_config from PixivServer.models.pixiv_metadata import ( - PixivMemberPortfolio, + PixivDateInfo, PixivImageComplete, - PixivMasterMember, - PixivMasterImage, - PixivMangaImage, - PixivMasterTag, - PixivTagTranslation, + PixivImageToSeries, PixivImageToTag, + PixivMangaImage, + PixivMasterImage, + PixivMasterMember, PixivMasterSeries, - PixivImageToSeries, - PixivDateInfo, + PixivMasterTag, + PixivMemberPortfolio, + PixivSeriesInfo, PixivTagInfo, - PixivSeriesInfo + PixivTagTranslation, ) logger = logging.getLogger(__name__) @@ -108,6 +108,46 @@ def get_member_data_by_id(self, member_id: int) -> PixivMemberPortfolio: if cursor: cursor.close() + def count_members(self) -> int: + cursor = self.connection.cursor() + try: + cursor.execute("SELECT COUNT(*) FROM pixiv_master_member") + return cursor.fetchone()[0] + finally: + cursor.close() + + def count_artworks(self) -> int: + cursor = self.connection.cursor() + try: + cursor.execute("SELECT COUNT(*) FROM pixiv_master_image") + return cursor.fetchone()[0] + finally: + cursor.close() + + def count_pages(self) -> int: + cursor = self.connection.cursor() + try: + cursor.execute("SELECT COUNT(*) FROM pixiv_manga_image") + return cursor.fetchone()[0] + finally: + cursor.close() + + def count_tags(self) -> int: + cursor = self.connection.cursor() + try: + cursor.execute("SELECT COUNT(*) FROM pixiv_master_tag") + return cursor.fetchone()[0] + finally: + cursor.close() + + def count_series(self) -> int: + cursor = self.connection.cursor() + try: + cursor.execute("SELECT COUNT(*) FROM pixiv_master_series") + return cursor.fetchone()[0] + finally: + cursor.close() + def get_all_pixiv_member_ids(self) -> list[int]: """ Get all member IDs from the database. diff --git a/PixivServer/repository/subscription.py b/PixivServer/repository/subscription.py index 0e15ffd..ecc452a 100644 --- a/PixivServer/repository/subscription.py +++ b/PixivServer/repository/subscription.py @@ -10,11 +10,11 @@ class SubscriptionRepository: def __init__(self): self.db_path = pixivutil_config.db_path self.connection: sqlite3.Connection = None - + def open(self): self.connection = sqlite3.connect(self.db_path) self.create_table() - + def create_table(self): c = self.connection.cursor() c.execute(''' @@ -96,7 +96,7 @@ def add_member_subscription(self, member_id: int, member_name: str) -> bool: finally: c.close() return True - + def remove_member_subscription(self, member_id: int) -> bool: try: c = self.connection.cursor() @@ -142,14 +142,14 @@ def select_tag_subscriptions(self) -> list[tuple[str]]: raise e finally: c.close() - + return results def add_tag_subscription(self, tag_id: str, bookmark_count: int) -> bool: try: c = self.connection.cursor() c.execute( - '''INSERT INTO pixiv_server_tag_subscription (tag_id, bookmark_count, created_date, last_modified_date) + '''INSERT INTO pixiv_server_tag_subscription (tag_id, bookmark_count, created_date, last_modified_date) VALUES(?, ?, datetime('now'), datetime('now')) ON CONFLICT(tag_id) DO UPDATE SET diff --git a/PixivServer/routers/database.py b/PixivServer/routers/database.py index 028bd49..0f2b10b 100644 --- a/PixivServer/routers/database.py +++ b/PixivServer/routers/database.py @@ -1,8 +1,9 @@ +import json import logging import sqlite3 + from fastapi import APIRouter, Response from fastapi.encoders import jsonable_encoder -import json from PixivServer.repository.pixivutil import PixivUtilRepository @@ -31,10 +32,10 @@ def get_all_pixiv_member_ids() -> Response: content="Database error occurred.", status_code=500, ) - except Exception as e: - logger.error(f"Unexpected error while getting all member IDs: {e}") + except (TypeError, ValueError, RecursionError) as e: + logger.error(f"Serialization error while getting all member IDs: {e}") return Response( - content="An unexpected error occurred.", + content="Response serialization error occurred.", status_code=500, ) finally: @@ -62,10 +63,10 @@ def get_all_pixiv_image_ids() -> Response: content="Database error occurred.", status_code=500, ) - except Exception as e: - logger.error(f"Unexpected error while getting all image IDs: {e}") + except (TypeError, ValueError, RecursionError) as e: + logger.error(f"Serialization error while getting all image IDs: {e}") return Response( - content="An unexpected error occurred.", + content="Response serialization error occurred.", status_code=500, ) finally: @@ -93,10 +94,10 @@ def get_all_pixiv_tags() -> Response: content="Database error occurred.", status_code=500, ) - except Exception as e: - logger.error(f"Unexpected error while getting all tag IDs: {e}") + except (TypeError, ValueError, RecursionError) as e: + logger.error(f"Serialization error while getting all tag IDs: {e}") return Response( - content="An unexpected error occurred.", + content="Response serialization error occurred.", status_code=500, ) finally: @@ -124,10 +125,10 @@ def get_all_pixiv_series() -> Response: content="Database error occurred.", status_code=500, ) - except Exception as e: - logger.error(f"Unexpected error while getting all series IDs: {e}") + except (TypeError, ValueError, RecursionError) as e: + logger.error(f"Serialization error while getting all series IDs: {e}") return Response( - content="An unexpected error occurred.", + content="Response serialization error occurred.", status_code=500, ) finally: @@ -161,10 +162,10 @@ def get_pixiv_tag_info_by_id(tag_id: str) -> Response: content="Database error occurred.", status_code=500, ) - except Exception as e: - logger.error(f"Unexpected error while getting tag {tag_id}: {e}") + except (TypeError, ValueError, RecursionError) as e: + logger.error(f"Serialization error while getting tag {tag_id}: {e}") return Response( - content="An unexpected error occurred.", + content="Response serialization error occurred.", status_code=500, ) finally: @@ -198,10 +199,10 @@ def get_pixiv_series_info_by_id(series_id: str) -> Response: content="Database error occurred.", status_code=500, ) - except Exception as e: - logger.error(f"Unexpected error while getting series {series_id}: {e}") + except (TypeError, ValueError, RecursionError) as e: + logger.error(f"Serialization error while getting series {series_id}: {e}") return Response( - content="An unexpected error occurred.", + content="Response serialization error occurred.", status_code=500, ) finally: @@ -247,10 +248,10 @@ def get_pixiv_member_portfolio_by_id(member_id: str | None) -> Response: content="Database error occurred.", status_code=500, ) - except Exception as e: - logger.error(f"Unexpected error while getting member {member_id}: {e}") + except (TypeError, ValueError, RecursionError) as e: + logger.error(f"Serialization error while getting member {member_id}: {e}") return Response( - content="An unexpected error occurred.", + content="Response serialization error occurred.", status_code=500, ) finally: @@ -296,10 +297,10 @@ def get_pixiv_image_data_by_id(image_id: str | None) -> Response: content="Database error occurred.", status_code=500, ) - except Exception as e: - logger.error(f"Unexpected error while getting image {image_id}: {e}") + except (TypeError, ValueError, RecursionError) as e: + logger.error(f"Serialization error while getting image {image_id}: {e}") return Response( - content="An unexpected error occurred.", + content="Response serialization error occurred.", status_code=500, ) finally: diff --git a/PixivServer/routers/download_queue.py b/PixivServer/routers/download_queue.py index 29e82c6..e5fa051 100644 --- a/PixivServer/routers/download_queue.py +++ b/PixivServer/routers/download_queue.py @@ -1,17 +1,28 @@ -from datetime import timedelta import datetime +import logging import sqlite3 +import urllib.parse +from datetime import timedelta + from celery.result import AsyncResult -import logging from fastapi import APIRouter, Response from fastapi.responses import JSONResponse -import urllib.parse +from pixivutil_server_common.models import TagSortOrder, TagTypeMode +from PixivServer.models.pixiv_worker import ( + DeleteArtworkByIdRequest, + DownloadArtworkByIdRequest, + DownloadArtworksByMemberIdRequest, + DownloadArtworksByTagsRequest, +) from PixivServer.repository.pixivutil import PixivUtilRepository -from PixivServer.worker import delete_artwork_by_id, download_artworks_by_id, download_artworks_by_member_id, download_artworks_by_tag -from PixivServer.models.pixiv_worker import DeleteArtworkByIdRequest, DownloadArtworkByIdRequest, DownloadArtworksByMemberIdRequest, DownloadArtworksByTagsRequest from PixivServer.utils import is_valid_date -from pixivutil_server_common.models import TagSortOrder, TagTypeMode +from PixivServer.worker import ( + delete_artwork_by_id, + download_artworks_by_id, + download_artworks_by_member_id, + download_artworks_by_tag, +) logger = logging.getLogger('uvicorn.pixivutil') router = APIRouter() @@ -79,7 +90,7 @@ async def queue_download_artworks_by_member_id(member_id: str) -> Response: @router.post("/tag/{tag_name}") async def queue_download_artworks_by_tag( - tag_name: str, + tag_name: str, bookmark_count: int | None = None, sort_order: TagSortOrder = 'date_d', type_mode: TagTypeMode = 'a', diff --git a/PixivServer/routers/health.py b/PixivServer/routers/health.py index 2e31dd3..e28f1b0 100644 --- a/PixivServer/routers/health.py +++ b/PixivServer/routers/health.py @@ -1,4 +1,5 @@ import logging + from fastapi import APIRouter, Depends, Response import PixivServer.auth @@ -23,7 +24,7 @@ async def pixiv_health_check( cookie = pixiv.service.get_pixiv_cookie() pixiv_cookie_is_valid = pixiv.service.login_pixiv(cookie) - + if not pixiv_cookie_is_valid: return Response( content="Pixiv login failed.", diff --git a/PixivServer/routers/metadata_queue.py b/PixivServer/routers/metadata_queue.py index bfe78da..017b267 100644 --- a/PixivServer/routers/metadata_queue.py +++ b/PixivServer/routers/metadata_queue.py @@ -4,6 +4,7 @@ from celery.result import AsyncResult from fastapi import APIRouter from fastapi.responses import JSONResponse +from pixivutil_server_common.models import TagMetadataFilterMode from PixivServer.models.pixiv_worker import ( DownloadArtworkMetadataByIdRequest, @@ -17,7 +18,6 @@ download_series_metadata_by_id, download_tag_metadata_by_id, ) -from pixivutil_server_common.models import TagMetadataFilterMode logger = logging.getLogger("uvicorn.pixivutil") router = APIRouter() diff --git a/PixivServer/routers/metrics.py b/PixivServer/routers/metrics.py new file mode 100644 index 0000000..cbeb179 --- /dev/null +++ b/PixivServer/routers/metrics.py @@ -0,0 +1,13 @@ +import logging + +from fastapi import APIRouter +from fastapi.responses import Response +from prometheus_client import CONTENT_TYPE_LATEST, generate_latest + +logger = logging.getLogger('uvicorn.pixivutil') +router = APIRouter() + + +@router.get("") +async def prometheus_metrics() -> Response: + return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST) diff --git a/PixivServer/routers/server.py b/PixivServer/routers/server.py index 2839139..7750d11 100644 --- a/PixivServer/routers/server.py +++ b/PixivServer/routers/server.py @@ -1,7 +1,8 @@ import logging -from fastapi import APIRouter, Response +from fastapi import APIRouter, Response from pixivutil_server_common.models import UpdateCookieRequest + from PixivServer.service import pixiv logger = logging.getLogger('uvicorn.pixivutil') diff --git a/PixivServer/routers/subscription.py b/PixivServer/routers/subscription.py index 8e44cc5..9873743 100644 --- a/PixivServer/routers/subscription.py +++ b/PixivServer/routers/subscription.py @@ -1,4 +1,5 @@ import logging + from fastapi import APIRouter, Response from fastapi.responses import JSONResponse from pydantic import BaseModel diff --git a/PixivServer/service/pixiv.py b/PixivServer/service/pixiv.py index 41bbc6a..d8aba8e 100644 --- a/PixivServer/service/pixiv.py +++ b/PixivServer/service/pixiv.py @@ -1,37 +1,36 @@ +import logging import os import sqlite3 import sys import traceback - -import logging +from urllib.error import HTTPError sys.path.append('PixivUtil2') +from PixivServer.config.pixivutil import config as pixivutil_config from PixivServer.models.pixiv_worker import ( DeleteArtworkByIdRequest, DownloadArtworkByIdRequest, + DownloadArtworkMetadataByIdRequest, DownloadArtworksByMemberIdRequest, DownloadArtworksByTagsRequest, - DownloadArtworkMetadataByIdRequest, DownloadMemberMetadataByIdRequest, DownloadSeriesMetadataByIdRequest, DownloadTagMetadataByIdRequest, ) +from PixivServer.utils import clear_folder from PixivUtil2 import ( - PixivConfig, - PixivBrowserFactory, - PixivHelper, - PixivImageHandler, - PixivArtistHandler, - PixivDBManager, - PixivTagsHandler, - PixivException, + PixivArtistHandler, + PixivBrowserFactory, + PixivConfig, + PixivDBManager, + PixivException, + PixivHelper, + PixivImageHandler, + PixivTagsHandler, set_console_title, # noqa: F401, ) -from PixivServer.config.pixivutil import config as pixivutil_config -from PixivServer.utils import clear_folder - logger = logging.getLogger(__name__) # ------ START CALLER ITEMS ------ @@ -40,12 +39,12 @@ configfile = ".pixivUtil2/conf/config.ini" __dbManager__ = None __br__: PixivBrowserFactory.PixivBrowser = None -__blacklistTags = list() -__suppressTags = list() +__blacklistTags = [] +__suppressTags = [] __log__ = None -__errorList = list() -__blacklistMembers = list() -__blacklistTitles = list() +__errorList = [] +__blacklistMembers = [] +__blacklistTitles = [] __valid_options = () __seriesDownloaded = [] @@ -88,7 +87,7 @@ def __init__(self) -> None: def load_environment_variables(self): "environment variable configuration" - + if pixivutil_config.cookie: __config__.cookie = pixivutil_config.cookie @@ -99,7 +98,7 @@ def open(self, validate_pixiv_login: bool = True): global __br__ global __config__ global __dbManager__ - global __log__ + global __log__ # download control if not os.path.exists(configfile): os.makedirs(os.path.dirname(configfile), exist_ok=True) @@ -114,7 +113,7 @@ def open(self, validate_pixiv_login: bool = True): # setup database os.makedirs(os.path.dirname(__config__.dbPath), exist_ok=True) self.open_database() - + if __br__ is None: __br__ = PixivBrowserFactory.getBrowser(config=__config__) @@ -161,15 +160,15 @@ def login_pixiv(self, cookie) -> bool: result = False try: result = __br__.loginUsingCookie(login_cookie=cookie) - except BaseException: + except (HTTPError, PixivException, AssertionError, ValueError) as e: logger.error(f'Error at doLogin(): {sys.exc_info()}') logger.error(traceback.format_exc()) - raise PixivException("Cannot Login!", PixivException.CANNOT_LOGIN) + raise PixivException("Cannot Login!", PixivException.CANNOT_LOGIN) from e return result def get_pixiv_cookie(self): return __config__.cookie - + def update_pixiv_cookie(self, new_cookie: str) -> bool: __config__.cookie = new_cookie __config__.writeConfig(path=configfile) @@ -186,7 +185,7 @@ def get_artwork_data(self, artwork_id: int): def get_member_name(self, member_id: int) -> str: data = self.get_member_data(member_id)[0] return data.artistName - + def get_artwork_name(self, artwork_id: int) -> str: data, response = self.get_artwork_data(artwork_id) if data is None: diff --git a/PixivServer/service/subscription.py b/PixivServer/service/subscription.py index 31a25d3..f9e2cd3 100644 --- a/PixivServer/service/subscription.py +++ b/PixivServer/service/subscription.py @@ -1,9 +1,9 @@ import logging import time -from PixivServer.service.pixiv import service as pixiv_service -from PixivServer.repository.subscription import SubscriptionRepository from PixivServer.repository.pixivutil import PixivUtilRepository +from PixivServer.repository.subscription import SubscriptionRepository +from PixivServer.service.pixiv import service as pixiv_service logger = logging.getLogger(__name__) @@ -40,13 +40,13 @@ def run_member_subscription_job(self) -> dict[str, list[str]]: subscribed_members = self.get_subscribed_members() num_new_artworks = 0 - new_artwork_titles_by_member_id = dict() + new_artwork_titles_by_member_id = {} for member in subscribed_members: member_id, member_name = member[0], member[1] image_id_list = self.pixivutil_db.select_image_id_list_by_member_id(member_id) if not image_id_list: - image_id_list = list() + image_id_list = [] image_id_pool = set(image_id_list) member_data = pixiv_service.get_member_data(member_id)[0] @@ -62,7 +62,7 @@ def run_member_subscription_job(self) -> dict[str, list[str]]: if image_id not in image_id_pool: print(f'Image id {image_id} is not in {image_id_pool}') if member_name not in new_artwork_titles_by_member_id: - new_artwork_titles_by_member_id[member_name] = list() + new_artwork_titles_by_member_id[member_name] = [] new_artwork_titles_by_member_id[member_name].append(image_id) pixiv_service.download_artwork_by_id(image_id) num_new_artworks += 1 @@ -76,7 +76,7 @@ def get_subscribed_members(self) -> list[tuple[int, str]]: subscribed_members = self.subscription_db.select_member_subscriptions() if subscribed_members is None: logger.error("Failed to get member subscriptions.") - return list() + return [] return subscribed_members def add_member_subscription(self, member_id: str) -> dict[str, str]: @@ -120,14 +120,14 @@ def delete_member_subscription(self, member_id: str): } else: logger.info(f"Subscription for member ID {member_id} does not exist.") - return dict() + return {} def get_subscribed_tags(self) -> list[tuple[str]]: logger.info("Getting tags subscribed to.") subscribed_tags = self.subscription_db.select_tag_subscriptions() if subscribed_tags is None: logger.error("Failed to get tag subscriptions.") - return list() + return [] return subscribed_tags def add_tag_subscription(self, tag_id: str, bookmark_count: int) -> dict[str, str]: @@ -145,6 +145,6 @@ def delete_tag_subscription(self, tag_id: str): } else: logger.info(f"Subscription for tag ID {tag_id} does not exist.") - return dict() + return {} service = SubscriptionService() diff --git a/PixivServer/utils.py b/PixivServer/utils.py index c69bbd6..0f5102f 100644 --- a/PixivServer/utils.py +++ b/PixivServer/utils.py @@ -2,7 +2,6 @@ import logging import os import shutil -import traceback from datetime import datetime logger = logging.getLogger(__name__) @@ -11,16 +10,38 @@ def get_version() -> str: return importlib.metadata.version("pixivutil-server") def clear_folder(folder: str) -> bool: + try: + filenames = os.listdir(folder) + except FileNotFoundError as e: + logger.error("Failed to clear folder '%s': folder not found (%s)", folder, e) + return False + except NotADirectoryError as e: + logger.error("Failed to clear folder '%s': path is not a directory (%s)", folder, e) + return False + except PermissionError as e: + logger.error("Failed to clear folder '%s': permission denied (%s)", folder, e) + return False + except OSError as e: + logger.error("Failed to clear folder '%s': OS error (%s)", folder, e) + return False - for filename in os.listdir(folder): + for filename in filenames: file_path = os.path.join(folder, filename) try: if os.path.isfile(file_path) or os.path.islink(file_path): os.unlink(file_path) elif os.path.isdir(file_path): shutil.rmtree(file_path) - except Exception: - logger.error("Failed to delete: ", traceback.format_exc()) + except FileNotFoundError as e: + logger.warning("Skipped deleting '%s': path no longer exists (%s)", file_path, e) + except PermissionError as e: + logger.error("Failed to delete '%s': permission denied (%s)", file_path, e) + except IsADirectoryError as e: + logger.error("Failed to delete '%s': expected file, found directory (%s)", file_path, e) + except NotADirectoryError as e: + logger.error("Failed to delete '%s': expected directory, found file (%s)", file_path, e) + except OSError as e: + logger.error("Failed to delete '%s': OS error (%s)", file_path, e) return True diff --git a/PixivServer/worker.py b/PixivServer/worker.py index 1780f95..432bcec 100644 --- a/PixivServer/worker.py +++ b/PixivServer/worker.py @@ -1,20 +1,22 @@ +import logging import random import time import traceback + from celery import Celery from celery.signals import setup_logging, worker_init, worker_shutdown -import logging import PixivServer + # from PixivServer.config.worker import config as worker_config import PixivServer.service import PixivServer.service.pixiv from PixivServer.models.pixiv_worker import ( DeleteArtworkByIdRequest, DownloadArtworkByIdRequest, + DownloadArtworkMetadataByIdRequest, DownloadArtworksByMemberIdRequest, DownloadArtworksByTagsRequest, - DownloadArtworkMetadataByIdRequest, DownloadMemberMetadataByIdRequest, DownloadSeriesMetadataByIdRequest, DownloadTagMetadataByIdRequest, diff --git a/PixivServerCommon/pyproject.toml b/PixivServerCommon/pyproject.toml index 0123b31..65c1778 100644 --- a/PixivServerCommon/pyproject.toml +++ b/PixivServerCommon/pyproject.toml @@ -15,6 +15,14 @@ dependencies = [ "pydantic>=2.12.4", ] +[tool.ruff.lint] +select = ["A", "BLE", "F", "W", "E", "I", "UP", "C4", "FA", "ISC", "ICN", "SIM", "NPY"] +extend-select = ["ASYNC", "C4", "SIM"] +ignore = ["E501"] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] + [tool.uv.build-backend] module-name = "pixivutil_server_common" module-root = "" diff --git a/PixivUtilClient/pixivutil_client/__init__.py b/PixivUtilClient/pixivutil_client/__init__.py index e0a5f24..2fbde4b 100644 --- a/PixivUtilClient/pixivutil_client/__init__.py +++ b/PixivUtilClient/pixivutil_client/__init__.py @@ -1,5 +1,9 @@ from pixivutil_client.client import PixivAsyncClient -from pixivutil_client.exceptions import PixivAPIError, PixivClientError, PixivTransportError +from pixivutil_client.exceptions import ( + PixivAPIError, + PixivClientError, + PixivTransportError, +) __all__ = [ "PixivAsyncClient", diff --git a/PixivUtilClient/pixivutil_client/client.py b/PixivUtilClient/pixivutil_client/client.py index 5e73324..233f1da 100644 --- a/PixivUtilClient/pixivutil_client/client.py +++ b/PixivUtilClient/pixivutil_client/client.py @@ -37,7 +37,7 @@ def __init__( self._session = session self._owns_session = session is None - async def __aenter__(self) -> "PixivAsyncClient": + async def __aenter__(self) -> PixivAsyncClient: await self._ensure_session() return self diff --git a/PixivUtilClient/pyproject.toml b/PixivUtilClient/pyproject.toml index f9798ec..ea43339 100644 --- a/PixivUtilClient/pyproject.toml +++ b/PixivUtilClient/pyproject.toml @@ -17,6 +17,14 @@ dependencies = [ "pixivutil-server-common>=0.1.0", ] +[tool.ruff.lint] +select = ["A", "BLE", "F", "W", "E", "I", "UP", "C4", "FA", "ISC", "ICN", "SIM", "NPY"] +extend-select = ["ASYNC", "C4", "SIM"] +ignore = ["E501"] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] + [tool.uv.build-backend] module-name = "pixivutil_client" module-root = "" diff --git a/pyproject.toml b/pyproject.toml index fd94e1e..c251cb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,8 @@ dependencies = [ "celery>=5.5.3", "fastapi>=0.122.0", "pixivutil-server-common>=0.1.0", + "prometheus-client>=0.24.1", + "psutil>=7.2.2", "uvicorn[standard]>=0.38.0", ] @@ -36,11 +38,25 @@ pixivutil2 = [ "pysocks>=1.7.1", ] +[tool.pyright] +pythonVersion = "3.12" +include = ["PixivServer", "PixivServerCommon", "PixivUtilClient", "tests"] +exclude = ["PixivUtil2"] +reportUnusedCoroutine = "error" + [tool.ruff] exclude = [ "PixivUtil2/*" ] +[tool.ruff.lint] +select = ["A", "BLE", "F", "W", "E", "I", "UP", "C4", "FA", "ISC", "ICN", "SIM", "NPY"] +extend-select = ["ASYNC", "C4", "SIM"] +ignore = ["E501"] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] + [tool.uv.build-backend] module-name = "PixivServer" module-root = "" @@ -56,6 +72,7 @@ pixivutil-server-common = { workspace = true } [dependency-groups] dev = [ + "pyright>=1.1.408", "pytest>=9.0.2", "pytest-asyncio>=1.3.0", "ruff>=0.15.1", diff --git a/tests/conftest.py b/tests/conftest.py index 35e3eec..a601177 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,8 @@ -import pytest import tempfile from pathlib import Path +import pytest + @pytest.fixture def temp_dir(): diff --git a/tests/test_utils.py b/tests/test_utils.py index c25325f..67ca718 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -1,6 +1,6 @@ import pytest -from PixivServer.utils import get_version, clear_folder, is_valid_date +from PixivServer.utils import clear_folder, get_version, is_valid_date def test_get_version(): diff --git a/uv.lock b/uv.lock index 0350c35..fa7666c 100644 --- a/uv.lock +++ b/uv.lock @@ -807,6 +807,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + [[package]] name = "packaging" version = "26.0" @@ -894,6 +903,8 @@ dependencies = [ { name = "celery" }, { name = "fastapi" }, { name = "pixivutil-server-common" }, + { name = "prometheus-client" }, + { name = "psutil" }, { name = "uvicorn", extra = ["standard"] }, ] @@ -913,6 +924,7 @@ pixivutil2 = [ [package.dev-dependencies] dev = [ + { name = "pyright" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "ruff" }, @@ -933,6 +945,8 @@ requires-dist = [ { name = "mechanize", marker = "extra == 'pixivutil2'", specifier = ">=0.4.5" }, { name = "pillow", marker = "extra == 'pixivutil2'", specifier = ">=8.3.0" }, { name = "pixivutil-server-common", editable = "PixivServerCommon" }, + { name = "prometheus-client", specifier = ">=0.24.1" }, + { name = "psutil", specifier = ">=7.2.2" }, { name = "pysocks", marker = "extra == 'pixivutil2'", specifier = ">=1.7.1" }, { name = "uvicorn", extras = ["standard"], specifier = ">=0.38.0" }, ] @@ -940,6 +954,7 @@ provides-extras = ["pixivutil2"] [package.metadata.requires-dev] dev = [ + { name = "pyright", specifier = ">=1.1.408" }, { name = "pytest", specifier = ">=9.0.2" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "ruff", specifier = ">=0.15.1" }, @@ -996,6 +1011,15 @@ 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]] +name = "prometheus-client" +version = "0.24.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f0/58/a794d23feb6b00fc0c72787d7e87d872a6730dd9ed7c7b3e954637d8f280/prometheus_client-0.24.1.tar.gz", hash = "sha256:7e0ced7fbbd40f7b84962d5d2ab6f17ef88a72504dcf7c0b40737b43b2a461f9", size = 85616, upload-time = "2026-01-14T15:26:26.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/c3/24a2f845e3917201628ecaba4f18bab4d18a337834c1df2a159ee9d22a42/prometheus_client-0.24.1-py3-none-any.whl", hash = "sha256:150db128af71a5c2482b36e588fc8a6b95e498750da4b17065947c16070f4055", size = 64057, upload-time = "2026-01-14T15:26:24.42Z" }, +] + [[package]] name = "prompt-toolkit" version = "3.0.52" @@ -1092,6 +1116,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -1235,6 +1287,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] +[[package]] +name = "pyright" +version = "1.1.408" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nodeenv" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/74/b2/5db700e52554b8f025faa9c3c624c59f1f6c8841ba81ab97641b54322f16/pyright-1.1.408.tar.gz", hash = "sha256:f28f2321f96852fa50b5829ea492f6adb0e6954568d1caa3f3af3a5f555eb684", size = 4400578, upload-time = "2026-01-08T08:07:38.795Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/82/a2c93e32800940d9573fb28c346772a14778b84ba7524e691b324620ab89/pyright-1.1.408-py3-none-any.whl", hash = "sha256:090b32865f4fdb1e0e6cd82bf5618480d48eecd2eb2e70f960982a3d9a4c17c1", size = 6399144, upload-time = "2026-01-08T08:07:37.082Z" }, +] + [[package]] name = "pysocks" version = "1.7.1"