feature: now using uv + moved to asyncpg from psycopg2

This commit is contained in:
2026-07-10 15:03:42 +03:00
parent ccec4cdf27
commit 2e5e0ed8ca
32 changed files with 1702 additions and 964 deletions
+19
View File
@@ -6,6 +6,25 @@ compose.yaml
compose.yml
# Git
.git
.gitignore
.github
# Python / uv
.venv
__pycache__
*.pyc
*.pyo
*.pyd
.python-version
# Tests
test.py
test/
test.sql
tables.sql
# Docs / env
*.md
.env
example.env
+20 -8
View File
@@ -3,25 +3,31 @@ services:
image: git.frik.su/Beesquit/picrinth-server:latest
container_name: prcrinth-server
environment:
# This should be configured
access_token_expiration_time: 10080
# Configuration
secret_key: "your-key"
swagger_enabled: true
access_token_expiration_time: 10080
join_code_length: 8
# This part should not be touched. Probably
swagger_enabled: false
# Extra configuration (don't touch if you don't know what this is)
algorithm: "HS256"
db_host: "127.0.0.1" # review this later
db_port: 5434
# DB (postgres) settings
# change if you have another instance of postgress you want to use
db_host: "picrinth-postgres"
db_port: 5432
db_name: "picrinth"
db_user: "postgres"
db_password: "postgres"
ports:
- 8000:8000
volumes:
- ./data:/app/data
restart: unless-stopped
networks:
- local
postgres:
image: postgres:latest
container_name: picrinth_postgres
container_name: picrinth-postgres
environment:
POSTGRES_USER: "postgres"
POSTGRES_PASSWORD: "postgres"
@@ -29,7 +35,7 @@ services:
PGDATA: /var/lib/postgresql/data/pgdata
POSTGRES_INITDB_ARGS: "--auth-host=scram-sha-256"
ports:
- 5432:5432
- :5432
volumes:
- ./pgdata:/var/lib/postgresql/data/pgdata
healthcheck:
@@ -40,7 +46,13 @@ services:
restart: unless-stopped
tty: true
stdin_open: true
networks:
- local
volumes:
pgdata:
driver: local
networks:
local:
driver: bridge
+19 -5
View File
@@ -1,9 +1,23 @@
FROM python:3.13-slim
FROM python:3.13-slim AS base
ENV UV_LINK_MODE=copy \
UV_COMPILE_BYTECODE=1 \
UV_PYTHON_DOWNLOADS=never \
PYTHONUNBUFFERED=1 \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONPATH=/app/src
COPY --from=ghcr.io/astral-sh/uv:0.11.23 /uv /uvx /usr/local/bin/
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY pyproject.toml uv.lock ./
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-install-project --no-dev
COPY . .
CMD ["python", "src/__main__.py"]
COPY src src
RUN --mount=type=cache,target=/root/.cache/uv \
uv sync --frozen --no-dev
CMD ["uv", "run", "python", "src/__main__.py"]
+53
View File
@@ -0,0 +1,53 @@
[project]
name = "picrinth-server"
version = "0.1.0"
description = "Server part for the Picrinth app"
requires-python = ">=3.13"
readme = "README.md"
authors = [
{ name = "Beesquit" }
]
dependencies = [
"annotated-types==0.7.0",
"anyio==4.9.0",
"asyncpg==0.30.0",
"attrs==25.3.0",
"bcrypt==4.3.0",
"certifi==2025.8.3",
"charset-normalizer==3.4.2",
"click==8.2.1",
"cloudscraper==1.2.71",
"fastapi==0.116.1",
"gppt==4.1.1",
"h11==0.16.0",
"idna==3.10",
"loguru==0.7.3",
"outcome==1.3.0.post0",
"passlib==1.7.4",
"pixivpy3==3.7.5",
"pwinput==1.0.3",
"pydantic==2.11.7",
"pydantic-core==2.33.2",
"pyjwt==2.10.1",
"pyparsing==3.2.3",
"pysocks==1.7.1",
"python-decouple==3.8",
"python-multipart==0.0.20",
"requests==2.32.4",
"requests-toolbelt==1.0.0",
"selenium==4.34.2",
"sniffio==1.3.1",
"sortedcontainers==2.4.0",
"starlette==0.47.2",
"trio==0.30.0",
"trio-websocket==0.12.2",
"typing-extensions==4.14.1",
"typing-inspection==0.4.1",
"urllib3==2.5.0",
"uvicorn==0.35.0",
"websocket-client==1.8.0",
"wsproto==1.2.0",
]
[tool.uv]
package = false
-39
View File
@@ -1,39 +0,0 @@
annotated-types==0.7.0
anyio==4.9.0
attrs==25.3.0
bcrypt==4.3.0
certifi==2025.8.3
charset-normalizer==3.4.2
click==8.2.1
cloudscraper==1.2.71
fastapi==0.116.1
gppt==4.1.1
h11==0.16.0
idna==3.10
loguru==0.7.3
outcome==1.3.0.post0
passlib==1.7.4
pixivpy3==3.7.5
psycopg2-binary==2.9.10
pwinput==1.0.3
pydantic==2.11.7
pydantic_core==2.33.2
PyJWT==2.10.1
pyparsing==3.2.3
PySocks==1.7.1
python-decouple==3.8
python-multipart==0.0.20
requests==2.32.4
requests-toolbelt==1.0.0
selenium==4.34.2
sniffio==1.3.1
sortedcontainers==2.4.0
starlette==0.47.2
trio==0.30.0
trio-websocket==0.12.2
typing-inspection==0.4.1
typing_extensions==4.14.1
urllib3==2.5.0
uvicorn==0.35.0
websocket-client==1.8.0
wsproto==1.2.0
+13 -13
View File
@@ -1,7 +1,7 @@
from typing import Annotated
from asyncpg import Connection
from fastapi import APIRouter, Depends, HTTPException, status
from psycopg2._psycopg import connection
import db.accounts as db
import settings.settings as settings
@@ -17,22 +17,22 @@ accounts_router = APIRouter(prefix="/api/accounts", tags=["accounts"])
@accounts_router.post("/group")
async def read_accounts_by_group(
groupname: str,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if not check_group_author(conn, groupname, current_user.username) and current_user.role not in settings.settings.admin_roles:
if not await check_group_author(conn, groupname, current_user.username) and current_user.role not in settings.settings.admin_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not allowed",
)
return db.get_accounts_by_group(conn, groupname)
return await db.get_accounts_by_group(conn, groupname)
@accounts_router.post("/group/platform")
async def read_accounts_by_group_platform(
groupname: str,
platform: str,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if current_user.role not in settings.settings.admin_roles:
@@ -40,7 +40,7 @@ async def read_accounts_by_group_platform(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not allowed",
)
account_data = db.get_accounts_by_group_platform(conn, groupname, platform.lower())
account_data = await db.get_accounts_by_group_platform(conn, groupname, platform.lower())
if account_data is None:
return HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -56,21 +56,21 @@ async def add_account(
login: str,
password: str,
metadata: dict,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if not check_group_author(conn, groupname, current_user.username) and current_user.role not in settings.settings.admin_roles:
if not await check_group_author(conn, groupname, current_user.username) and current_user.role not in settings.settings.admin_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not allowed",
)
if db.check_account_existence(conn, groupname, platform.lower()):
if await db.check_account_existence(conn, groupname, platform.lower()):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Account already exists",
)
hashed_password = encode_str(password)
return db.create_account(conn, groupname, current_user.username, platform.lower(), login, hashed_password, metadata)
return await db.create_account(conn, groupname, current_user.username, platform.lower(), login, hashed_password, metadata)
@accounts_router.post("/update")
@@ -81,10 +81,10 @@ async def update_account(
login: str,
password: str,
metadata: dict,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
account_data = db.get_accounts_by_group_platform(conn, groupname, platform.lower())
account_data = await db.get_accounts_by_group_platform(conn, groupname, platform.lower())
if account_data is None:
return HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -97,4 +97,4 @@ async def update_account(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not allowed",
)
return db.update_account(conn, groupname, author, platform.lower(), login, password, metadata)
return await db.update_account(conn, groupname, author, platform.lower(), login, password, metadata)
+7 -7
View File
@@ -1,7 +1,7 @@
from typing import Annotated
from asyncpg import Connection
from fastapi import APIRouter, Depends, HTTPException, status
from psycopg2._psycopg import connection
import db.users as db
import settings.settings as settings
@@ -15,36 +15,36 @@ anon_router = APIRouter(prefix="/api/anon", tags=["anon"])
async def add_admin(
username: str,
password: str,
conn: Annotated[connection, Depends(get_db_connection)]
conn: Annotated[Connection, Depends(get_db_connection)]
):
if not settings.settings.allow_create_admins:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not allowed",
)
if db.check_user_existence(conn, username):
if await db.check_user_existence(conn, username):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="User already exists",
)
hashed_password = get_password_hash(password)
return db.create_user(conn, username, hashed_password, "admin")
return await db.create_user(conn, username, hashed_password, "admin")
@anon_router.post("/add/user")
async def add_user(
username: str,
password: str,
conn: Annotated[connection, Depends(get_db_connection)]
conn: Annotated[Connection, Depends(get_db_connection)]
):
if not settings.settings.allow_create_users:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not allowed",
)
if db.check_user_existence(conn, username):
if await db.check_user_existence(conn, username):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="User already exists",
)
hashed_password = get_password_hash(password)
return db.create_user(conn, username, hashed_password, "user")
return await db.create_user(conn, username, hashed_password, "user")
+4 -4
View File
@@ -1,9 +1,9 @@
from datetime import timedelta
from typing import Annotated
from asyncpg import Connection
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordRequestForm
from psycopg2._psycopg import connection
from api.models import Token
from api.utils import authenticate_user, create_access_token
@@ -17,9 +17,9 @@ auth_router = APIRouter(prefix="/api", tags=["auth"])
@auth_router.post("/token")
async def login(
form_data: Annotated[OAuth2PasswordRequestForm, Depends()],
conn: Annotated[connection, Depends(get_db_connection)]
conn: Annotated[Connection, Depends(get_db_connection)]
) -> Token:
password_correct = authenticate_user(conn, form_data.username, form_data.password)
password_correct = await authenticate_user(conn, form_data.username, form_data.password)
if not password_correct:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -27,7 +27,7 @@ async def login(
headers={"WWW-Authenticate": "Bearer"},
)
user_disabled = check_user_disabled(conn, form_data.username)
user_disabled = await check_user_disabled(conn, form_data.username)
if user_disabled:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
+12 -12
View File
@@ -1,7 +1,7 @@
from typing import Annotated
from asyncpg import Connection
from fastapi import APIRouter, Depends, HTTPException, status
from psycopg2._psycopg import connection
import db.feeds as db
import settings.settings as settings
@@ -20,10 +20,10 @@ feeds_router = APIRouter(prefix="/api/feeds", tags=["feeds"])
@feeds_router.post("/feed")
async def read_feed(
feed_id: int,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
feed_data = db.get_feed(conn, feed_id)
feed_data = await db.get_feed(conn, feed_id)
if feed_data is None:
return HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -31,14 +31,14 @@ async def read_feed(
)
feed = Feed().fill(feed_data)
groupname = get_groupname_by_feed_id(conn, feed_id)
groupname = await get_groupname_by_feed_id(conn, feed_id)
if groupname is None:
return HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No such feed",
)
if not check_membership_exists(conn, current_user.username, groupname) and current_user.role not in settings.settings.admin_roles:
if not await check_membership_exists(conn, current_user.username, groupname) and current_user.role not in settings.settings.admin_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not allowed",
@@ -51,19 +51,19 @@ async def read_feed(
@feeds_router.post("/new")
async def new_feed(
groupname: str,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if not check_group_author(conn, groupname, current_user.username) and current_user.role not in settings.settings.admin_roles:
if not await check_group_author(conn, groupname, current_user.username) and current_user.role not in settings.settings.admin_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not allowed",
)
accounts = get_accounts_by_group(conn, groupname)
feed = generate_feed(conn, accounts)
accounts = await get_accounts_by_group(conn, groupname)
feed = await generate_feed(conn, accounts)
if feed:
return db.create_feed(conn, groupname, feed)
return await db.create_feed(conn, groupname, feed)
else:
raise HTTPException(
status_code=status.HTTP_424_FAILED_DEPENDENCY,
@@ -75,11 +75,11 @@ async def new_feed(
@feeds_router.post("/delete")
async def delete_feed(
feed_id: int,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if current_user.role in settings.settings.admin_roles:
return db.delete_feed(conn, feed_id)
return await db.delete_feed(conn, feed_id)
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
+39 -39
View File
@@ -1,8 +1,8 @@
import secrets
from typing import Annotated
from asyncpg import Connection
from fastapi import APIRouter, Depends, HTTPException, status
from psycopg2._psycopg import connection
import db.groups as db
import settings.settings as settings
@@ -19,7 +19,7 @@ groups_router = APIRouter(prefix="/api/groups", tags=["groups"])
@groups_router.post("/group")
async def read_any_group(
groupname: str,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if current_user.role not in settings.settings.admin_roles:
@@ -27,7 +27,7 @@ async def read_any_group(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not allowed",
)
group_data = db.get_group(conn, groupname)
group_data = await db.get_group(conn, groupname)
if group_data is None:
return HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -38,15 +38,15 @@ async def read_any_group(
@groups_router.post("/invite_code")
async def read_group_invite_code(
groupname: str,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if not check_membership_exists(conn, current_user.username, groupname) and current_user.role not in settings.settings.admin_roles:
if not await check_membership_exists(conn, current_user.username, groupname) and current_user.role not in settings.settings.admin_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not allowed",
)
invite_code = db.get_group_invite_code(conn, groupname)
invite_code = await db.get_group_invite_code(conn, groupname)
if invite_code is None:
return HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -57,20 +57,20 @@ async def read_group_invite_code(
@groups_router.post("/last_feed")
async def read_group_last_feed_id(
groupname: str,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if not check_membership_exists(conn, current_user.username, groupname) and current_user.role not in settings.settings.admin_roles:
if not await check_membership_exists(conn, current_user.username, groupname) and current_user.role not in settings.settings.admin_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not allowed",
)
return db.get_group_last_feed_id(conn, groupname)
return await db.get_group_last_feed_id(conn, groupname)
@groups_router.post("/add")
async def add_group(
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)],
groupname: str,
allow_skips: bool = True,
@@ -82,30 +82,30 @@ async def add_group(
detail="Not allowed",
)
if db.check_group_existence(conn, groupname):
if await db.check_group_existence(conn, groupname):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Group already exists",
)
invite_code = "".join(secrets.choice(JOIN_CODE_SYMBOLS) for _ in range(startup_settings.join_code_length))
while db.check_invite_code(conn, invite_code):
while await db.check_invite_code(conn, invite_code):
invite_code = "".join(secrets.choice(JOIN_CODE_SYMBOLS) for _ in range(startup_settings.join_code_length))
return {
"result": db.create_group(conn, groupname, invite_code, current_user.username, allow_skips, feed_interval_minutes),
"result": await db.create_group(conn, groupname, invite_code, current_user.username, allow_skips, feed_interval_minutes),
"invite code": invite_code
}
@groups_router.post("/delete")
async def delete_group(
groupname: str,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if current_user.role in settings.settings.admin_roles:
return db.delete_group(conn, groupname)
if db.check_group_author(conn, groupname, current_user.username):
return db.delete_group(conn, groupname)
return await db.delete_group(conn, groupname)
if await db.check_group_author(conn, groupname, current_user.username):
return await db.delete_group(conn, groupname)
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -117,19 +117,19 @@ async def delete_group(
async def update_groupname(
groupname: str,
new_groupname: str,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if db.check_group_existence(conn, new_groupname):
if await db.check_group_existence(conn, new_groupname):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Groupname is already taken",
)
if current_user.role in settings.settings.admin_roles:
return db.update_group_groupname(conn, groupname, new_groupname)
if db.check_group_author(conn, groupname, current_user.username):
return db.update_group_groupname(conn, groupname, new_groupname)
return await db.update_group_groupname(conn, groupname, new_groupname)
if await db.check_group_author(conn, groupname, current_user.username):
return await db.update_group_groupname(conn, groupname, new_groupname)
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -140,13 +140,13 @@ async def update_groupname(
async def update_author(
groupname: str,
new_author: str,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if current_user.role in settings.settings.admin_roles:
return db.update_group_author(conn, groupname, new_author)
if db.check_group_author(conn, groupname, current_user.username):
return db.update_group_author(conn, groupname, new_author)
return await db.update_group_author(conn, groupname, new_author)
if await db.check_group_author(conn, groupname, current_user.username):
return await db.update_group_author(conn, groupname, new_author)
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -156,21 +156,21 @@ async def update_author(
@groups_router.get("/update/invite_code")
async def update_invite_code(
groupname: str,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
invite_code = "".join(secrets.choice(JOIN_CODE_SYMBOLS) for _ in range(startup_settings.join_code_length))
while db.check_invite_code(conn, invite_code):
while await db.check_invite_code(conn, invite_code):
invite_code = "".join(secrets.choice(JOIN_CODE_SYMBOLS) for _ in range(startup_settings.join_code_length))
if current_user.role in settings.settings.admin_roles:
return {
"result": db.update_group_invite_code(conn, groupname, invite_code),
"result": await db.update_group_invite_code(conn, groupname, invite_code),
"invite code": invite_code
}
if db.check_group_author(conn, groupname, current_user.username):
if await db.check_group_author(conn, groupname, current_user.username):
return {
"result": db.update_group_invite_code(conn, groupname, invite_code),
"result": await db.update_group_invite_code(conn, groupname, invite_code),
"invite code": invite_code
}
else:
@@ -184,13 +184,13 @@ async def update_invite_code(
async def update_allow_skips(
groupname: str,
allow_skips: bool,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if current_user.role in settings.settings.admin_roles:
return db.update_group_allow_skips(conn, groupname, allow_skips)
if db.check_group_author(conn, groupname, current_user.username):
return db.update_group_allow_skips(conn, groupname, allow_skips)
return await db.update_group_allow_skips(conn, groupname, allow_skips)
if await db.check_group_author(conn, groupname, current_user.username):
return await db.update_group_allow_skips(conn, groupname, allow_skips)
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -202,13 +202,13 @@ async def update_allow_skips(
async def update_feed_interval(
groupname: str,
feed_interval: int,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if current_user.role in settings.settings.admin_roles:
return db.update_group_feed_interval(conn, groupname, feed_interval)
if db.check_group_author(conn, groupname, current_user.username):
return db.update_group_feed_interval(conn, groupname, feed_interval)
return await db.update_group_feed_interval(conn, groupname, feed_interval)
if await db.check_group_author(conn, groupname, current_user.username):
return await db.update_group_feed_interval(conn, groupname, feed_interval)
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
+19 -19
View File
@@ -1,7 +1,7 @@
from typing import Annotated
from asyncpg import Connection
from fastapi import APIRouter, Depends, HTTPException, status
from psycopg2._psycopg import connection
import db.memberships as db
import settings.settings as settings
@@ -20,18 +20,18 @@ memberships_router = APIRouter(prefix="/api/membership", tags=["memberships"])
@memberships_router.get("/me")
async def read_users_groups(
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
return db.get_memberships_by_username(conn, current_user.username)
return await db.get_memberships_by_username(conn, current_user.username)
@memberships_router.post("/user")
async def read_users_any_memberships(
username: str,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if not check_user_existence(conn, username):
if not await check_user_existence(conn, username):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User does not exist",
@@ -43,35 +43,35 @@ async def read_users_any_memberships(
detail="Not allowed",
)
return db.get_memberships_by_username(conn, username)
return await db.get_memberships_by_username(conn, username)
@memberships_router.post("/group")
async def read_any_group_members(
groupname: str,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
user_is_in_group = db.check_membership_exists(conn, current_user.username, groupname)
user_is_in_group = await db.check_membership_exists(conn, current_user.username, groupname)
if not user_is_in_group and current_user.role not in settings.settings.admin_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not allowed",
)
if not check_group_existence(conn, groupname):
if not await check_group_existence(conn, groupname):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No such group",
)
return db.get_memberships_by_groupname(conn, groupname)
return await db.get_memberships_by_groupname(conn, groupname)
@memberships_router.post("/add")
async def add_membership(
username: str,
invite_code: str,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if username != current_user.username and current_user.role not in settings.settings.admin_roles:
@@ -80,39 +80,39 @@ async def add_membership(
detail="Not allowed",
)
groupname = get_groupname_by_invite_code(conn, invite_code)
groupname = await get_groupname_by_invite_code(conn, invite_code)
if groupname is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Invite code is incorrect",
)
if not check_user_existence(conn, username):
if not await check_user_existence(conn, username):
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User does not exist",
)
if db.check_membership_exists(conn, username, groupname):
if await db.check_membership_exists(conn, username, groupname):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="User is already a member",
)
return db.create_membership(conn, username, groupname)
return await db.create_membership(conn, username, groupname)
@memberships_router.post("/delete")
async def delete_membership(
username: str,
groupname: str,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if current_user.role in settings.settings.admin_roles:
return db.delete_membership(conn, username, groupname)
return await db.delete_membership(conn, username, groupname)
if check_group_author(conn, groupname, current_user.username):
return db.delete_membership(conn, username, groupname)
if await check_group_author(conn, groupname, current_user.username):
return await db.delete_membership(conn, username, groupname)
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
+7 -7
View File
@@ -1,7 +1,7 @@
from typing import Annotated
from asyncpg import Connection
from fastapi import APIRouter, Depends, HTTPException, status
from psycopg2._psycopg import connection
import db.pictures as db
import settings.settings as settings
@@ -15,10 +15,10 @@ pictures_router = APIRouter(prefix="/api/pictures", tags=["pictures"])
@pictures_router.post("/picture")
async def read_picture(
id: int,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
picture_data = db.get_picture(conn, id)
picture_data = await db.get_picture(conn, id)
if picture_data is None:
return HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -29,7 +29,7 @@ async def read_picture(
@pictures_router.post("/add")
async def add_picture(
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)],
source: str,
external_id: str,
@@ -43,18 +43,18 @@ async def add_picture(
)
return {
"id": db.create_picture(conn, source, external_id, url, metadata)
"id": await db.create_picture(conn, source, external_id, url, metadata)
}
@pictures_router.post("/delete")
async def delete_picture(
picture_id: int,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if current_user.role in settings.settings.admin_roles:
return db.delete_picture(conn, picture_id)
return await db.delete_picture(conn, picture_id)
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
+19 -19
View File
@@ -1,7 +1,7 @@
from typing import Annotated
from asyncpg import Connection
from fastapi import APIRouter, Depends, HTTPException, status
from psycopg2._psycopg import connection
import db.swipes as db
import settings.settings as settings
@@ -23,22 +23,22 @@ async def read_swipe(
username: str,
feed_id: int,
picture_id: int,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
groupname = get_groupname_by_feed_id(conn, feed_id)
groupname = await get_groupname_by_feed_id(conn, feed_id)
if groupname is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No such feed or feed is not linked to group",
)
if not check_membership_exists(conn, current_user.username, groupname) and current_user.role not in settings.settings.admin_roles:
if not await check_membership_exists(conn, current_user.username, groupname) and current_user.role not in settings.settings.admin_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not allowed",
)
swipe_data = db.get_swipe(conn, username, feed_id, picture_id)
swipe_data = await db.get_swipe(conn, username, feed_id, picture_id)
if swipe_data is None:
return HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -51,42 +51,42 @@ async def read_swipe(
async def read_swipes_by_picture_id(
feed_id: int,
picture_id: int,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
groupname = get_groupname_by_feed_id(conn, feed_id)
groupname = await get_groupname_by_feed_id(conn, feed_id)
if groupname is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No such feed or feed is not linked to group",
)
if not check_membership_exists(conn, current_user.username, groupname) and current_user.role not in settings.settings.admin_roles:
if not await check_membership_exists(conn, current_user.username, groupname) and current_user.role not in settings.settings.admin_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not allowed",
)
return db.get_swipes_by_picture_id(conn, picture_id, feed_id)
return await db.get_swipes_by_picture_id(conn, picture_id, feed_id)
@swipes_router.post("/swipe/user")
async def read_user_swipes(
username: str,
feed_id: int,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
groupname = get_groupname_by_feed_id(conn, feed_id)
groupname = await get_groupname_by_feed_id(conn, feed_id)
if groupname is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No such feed or feed is not linked to group",
)
if not check_membership_exists(conn, current_user.username, groupname) and current_user.role not in settings.settings.admin_roles:
if not await check_membership_exists(conn, current_user.username, groupname) and current_user.role not in settings.settings.admin_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not allowed",
)
return db.get_swipes_by_user(conn, username, feed_id)
return await db.get_swipes_by_user(conn, username, feed_id)
@swipes_router.post("/add")
@@ -94,17 +94,17 @@ async def add_swipe(
feed_id: int,
picture_id: int,
value: int,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
groupname = get_groupname_by_feed_id(conn, feed_id)
groupname = await get_groupname_by_feed_id(conn, feed_id)
if groupname is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No such feed or feed is not linked to group",
)
group_data = get_group(conn, groupname)
group_data = await get_group(conn, groupname)
if group_data is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -120,7 +120,7 @@ async def add_swipe(
detail="Skips are disallowed in the group",
)
result = db.create_swipe(conn, current_user.username, feed_id, picture_id, value)
result = await db.create_swipe(conn, current_user.username, feed_id, picture_id, value)
if result:
# TODO: call function to like picture on the platform
pass
@@ -132,11 +132,11 @@ async def delete_swipe(
username: str,
feed_id: int,
picture_id: int,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if current_user.role in settings.settings.admin_roles:
return db.delete_swipe(conn, username, feed_id, picture_id)
return await db.delete_swipe(conn, username, feed_id, picture_id)
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
+22 -22
View File
@@ -1,7 +1,7 @@
from typing import Annotated
from asyncpg import Connection
from fastapi import APIRouter, Depends, HTTPException, status
from psycopg2._psycopg import connection
import db.users as db
import settings.settings as settings
@@ -19,7 +19,7 @@ async def read_users_me(current_user: Annotated[User, Depends(get_current_user)]
@users_router.post("/user")
async def read_users_any(
username: str,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if current_user.role not in settings.settings.admin_roles:
@@ -27,7 +27,7 @@ async def read_users_any(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not allowed",
)
user_data = db.get_user(conn, username)
user_data = await db.get_user(conn, username)
if user_data is None:
return HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -41,7 +41,7 @@ async def read_users_any(
async def add_admin(
username: str,
password: str,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if not settings.settings.allow_create_admins_by_admins:
@@ -54,19 +54,19 @@ async def add_admin(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not allowed",
)
if db.check_user_existence(conn, username):
if await db.check_user_existence(conn, username):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="User already exists",
)
hashed_password = get_password_hash(password)
return db.create_user(conn, username, hashed_password, "admin")
return await db.create_user(conn, username, hashed_password, "admin")
@users_router.post("/add/user")
async def add_user(
username: str,
password: str,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if not settings.settings.allow_create_users and current_user.role not in settings.settings.admin_roles:
@@ -74,23 +74,23 @@ async def add_user(
status_code=status.HTTP_403_FORBIDDEN,
detail="Not allowed",
)
if db.check_user_existence(conn, username):
if await db.check_user_existence(conn, username):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="User already exists",
)
hashed_password = get_password_hash(password)
return db.create_user(conn, username, hashed_password, "user")
return await db.create_user(conn, username, hashed_password, "user")
@users_router.post("/delete")
async def delete_user(
username: str,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if current_user.username == username or current_user.role in settings.settings.admin_roles:
return db.delete_user(conn, username)
return await db.delete_user(conn, username)
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -102,11 +102,11 @@ async def delete_user(
async def update_disabled(
username: str,
disabled: bool,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if current_user.role in settings.settings.admin_roles:
return db.update_user_disabled(conn, username, disabled)
return await db.update_user_disabled(conn, username, disabled)
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -117,11 +117,11 @@ async def update_disabled(
async def update_role(
username: str,
role: str,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if current_user.role in settings.settings.admin_roles:
return db.update_user_role(conn, username, role)
return await db.update_user_role(conn, username, role)
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -133,16 +133,16 @@ async def update_role(
async def update_username(
username: str,
new_username: str,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if db.check_user_existence(conn, new_username):
if await db.check_user_existence(conn, new_username):
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Username is already taken",
)
if current_user.username == username or current_user.role in settings.settings.admin_roles:
return db.update_user_username(conn, username, new_username)
return await db.update_user_username(conn, username, new_username)
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -153,12 +153,12 @@ async def update_username(
async def update_password(
username: str,
password: str,
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
if current_user.username == username or current_user.role in settings.settings.admin_roles:
hashed_password = get_password_hash(password)
return db.update_user_password(conn, username, hashed_password)
return await db.update_user_password(conn, username, hashed_password)
else:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
@@ -168,7 +168,7 @@ async def update_password(
@users_router.get("/update/last_seen")
async def update_last_seen(
conn: Annotated[connection, Depends(get_db_connection)],
conn: Annotated[Connection, Depends(get_db_connection)],
current_user: Annotated[User, Depends(get_current_user)]
):
return db.update_user_last_seen(conn, current_user.username)
return await db.update_user_last_seen(conn, current_user.username)
+9 -9
View File
@@ -1,12 +1,12 @@
from datetime import datetime, timedelta, timezone
from typing import Annotated
from asyncpg import Connection
import bcrypt
import jwt
from fastapi import Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer
from jwt.exceptions import InvalidTokenError
from psycopg2._psycopg import connection
import db.groups
import db.users
@@ -30,15 +30,15 @@ def encode_token(payload):
return jwt.encode(payload, startup_settings.secret_key, algorithm=startup_settings.algorithm)
def authenticate_user(
conn: connection,
async def authenticate_user(
conn: Connection,
username: str,
user_password: str
):
if not user_password:
return False
db_user_password = db.users.get_user_password(conn, username)
db_user_password = await db.users.get_user_password(conn, username)
if db_user_password is None:
return False
@@ -60,7 +60,7 @@ def create_access_token(
async def get_current_user(
token: Annotated[str, Depends(oauth2_scheme)],
conn: Annotated[connection, Depends(get_db_connection)]
conn: Annotated[Connection, Depends(get_db_connection)]
):
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
@@ -77,7 +77,7 @@ async def get_current_user(
except InvalidTokenError:
raise credentials_exception
user_data = db.users.get_user(conn, token_data.username)
user_data = await db.users.get_user(conn, token_data.username)
if user_data is None:
raise credentials_exception
@@ -92,15 +92,15 @@ async def get_current_user(
return user
def get_group_by_name(
conn: connection,
async def get_group_by_name(
conn: Connection,
groupname: str
):
group_exception = HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="No such group"
)
group_data = db.groups.get_group(conn, groupname)
group_data = await db.groups.get_group(conn, groupname)
if group_data is None:
raise group_exception
+2 -1
View File
@@ -13,7 +13,7 @@ from api.memberships import memberships_router
from api.pictures import pictures_router
from api.swipes import swipes_router
from api.users import users_router
from db.internal import connect_db, disconnect_db
from db.internal import connect_db, disconnect_db, initialize_db
from settings import startup_settings
from settings.settings import settings_down, settings_up
from settings.startup_settings import setup_settings
@@ -41,6 +41,7 @@ def create_app():
app.add_event_handler("startup", setup_settings)
app.add_event_handler("startup", connect_db)
app.add_event_handler("startup", initialize_db)
app.add_event_handler("startup", settings_up)
app.include_router(general_router)
+87 -101
View File
@@ -1,14 +1,13 @@
import json
import psycopg2.extras
from psycopg2._psycopg import connection
from asyncpg import Connection
from api.models import Account
# account create and delete
def create_account(
conn: connection,
async def create_account(
conn: Connection,
groupname: str,
author: str,
platform: str,
@@ -16,65 +15,59 @@ def create_account(
password: str,
metadata: dict
):
with conn.cursor() as cur:
cur.execute(
"""
insert into picrinth.accounts
(groupname, author, platform, login,
password, metadata, created_at)
values (%s, %s, %s, %s, now())
returning id
""",
(groupname, author, platform, login, password, json.dumps(metadata)),
)
result = cur.fetchone()
conn.commit()
if result is None:
return None
return result[0]
result = await conn.fetchrow(
"""
insert into picrinth.accounts
(groupname, author, platform, login,
password, metadata, created_at)
values ($1, $2, $3, $4, $5, $6, now())
returning id
""",
groupname, author, platform, login, password, json.dumps(metadata),
)
if result is None:
return None
return result["id"]
def delete_account(
conn: connection,
async def delete_account(
conn: Connection,
account_id: str
):
with conn.cursor() as cur:
cur.execute(
"""
delete from picrinth.accounts
where account_id = %s
""",
(account_id),
)
conn.commit()
return cur.rowcount > 0
result = await conn.execute(
"""
delete from picrinth.accounts
where account_id = $1
""",
account_id,
)
return result != "DELETE 0"
# account checks
def check_account_existence(
conn: connection,
async def check_account_existence(
conn: Connection,
groupname: str,
platform: str
):
with conn.cursor() as cur:
cur.execute(
"""
select exists(
select 1
from picrinth.accounts
where groupname = %s and platform = %s
);
""",
(groupname, platform),
)
return cur.fetchone()[0] # type: ignore
row = await conn.fetchrow(
"""
select exists(
select 1
from picrinth.accounts
where groupname = $1 and platform = $2
);
""",
groupname, platform,
)
return row["exists"] # type: ignore
# account update
def update_account(
conn: connection,
async def update_account(
conn: Connection,
groupname: str,
author: str,
platform: str,
@@ -82,73 +75,66 @@ def update_account(
password: str,
metadata: dict
):
with conn.cursor() as cur:
cur.execute(
"""
update picrinth.accounts
SET author = %s,
login = %s,
password = %s,
metadata = %s
where groupname = %s and platform = %s
""",
(author, login, password, json.dumps(metadata), groupname, platform),
)
conn.commit()
return cur.rowcount > 0
result = await conn.execute(
"""
update picrinth.accounts
SET author = $1,
login = $2,
password = $3,
metadata = $4
where groupname = $5 and platform = $6
""",
author, login, password, json.dumps(metadata), groupname, platform,
)
return result != "UPDATE 0"
def update_account_metadata(
conn: connection,
async def update_account_metadata(
conn: Connection,
groupname: str,
platform: str,
metadata: dict
):
with conn.cursor() as cur:
cur.execute(
"""
update picrinth.accounts
SET metadata = %s
where groupname = %s and platform = %s
""",
(json.dumps(metadata), groupname, platform),
)
conn.commit()
return cur.rowcount > 0
result = await conn.execute(
"""
update picrinth.accounts
SET metadata = $1
where groupname = $2 and platform = $3
""",
json.dumps(metadata), groupname, platform,
)
return result != "UPDATE 0"
# account receiving
def get_accounts_by_group(
conn: connection,
async def get_accounts_by_group(
conn: Connection,
groupname: str
) -> list[Account]:
with conn.cursor() as cur:
cur.execute(
"""
select *
from picrinth.accounts
where groupname = %s
""",
(groupname,),
)
return [Account().fill(account_data) for (account_data,) in cur.fetchall()]
rows = await conn.fetch(
"""
select *
from picrinth.accounts
where groupname = $1
""",
groupname,
)
return [Account().fill(account_data) for account_data in rows]
def get_accounts_by_group_platform(
conn: connection,
async def get_accounts_by_group_platform(
conn: Connection,
groupname: str,
platform: str
):
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
cur.execute(
"""
select groupname, author,
platform, login, password,
metadata, created_at
from picrinth.accounts
where groupname = %s and platform = %s
""",
(groupname, platform),
)
return cur.fetchone()
return await conn.fetchrow(
"""
select groupname, author,
platform, login, password,
metadata, created_at
from picrinth.accounts
where groupname = $1 and platform = $2
""",
groupname, platform,
)
+51 -61
View File
@@ -1,83 +1,73 @@
import psycopg2.extras
from psycopg2._psycopg import connection
from asyncpg import Connection
# feed create and delete
def create_feed(
conn: connection,
async def create_feed(
conn: Connection,
groupname: str,
image_ids: list[int]
):
with conn.cursor() as cur:
cur.execute(
"""
insert into picrinth.feeds
(groupname, image_ids, created_at)
values (%s, %s, now())
returning id
""",
(groupname, image_ids),
)
result = cur.fetchone()
conn.commit()
if result is None:
return None
return result[0]
result = await conn.fetchrow(
"""
insert into picrinth.feeds
(groupname, image_ids, created_at)
values ($1, $2, now())
returning id
""",
groupname, image_ids,
)
if result is None:
return None
return result["id"]
def delete_feed(
conn: connection,
async def delete_feed(
conn: Connection,
feed_id: int
):
with conn.cursor() as cur:
cur.execute(
"""
delete from picrinth.feeds
where id = %s
""",
(feed_id,),
)
conn.commit()
return cur.rowcount > 0
result = await conn.execute(
"""
delete from picrinth.feeds
where id = $1
""",
feed_id,
)
return result != "DELETE 0"
# feed receiving
def get_feed(
conn: connection,
async def get_feed(
conn: Connection,
feed_id: int
):
with conn.cursor() as cur:
cur.execute(
"""
select groupname
from picrinth.feeds
where id = %s
""",
(feed_id,),
)
result = cur.fetchone()
if result is None:
return None
return cur.fetchone()[0] # type: ignore
result = await conn.fetchrow(
"""
select groupname
from picrinth.feeds
where id = $1
""",
feed_id,
)
if result is None:
return None
return result["groupname"] # type: ignore
# additional
def get_groupname_by_feed_id(
conn: connection,
async def get_groupname_by_feed_id(
conn: Connection,
feed_id: int
):
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
cur.execute(
"""
select groupname
from picrinth.feeds
where id = %s
""",
(feed_id,),
)
result = cur.fetchone()
if result is None:
return None
return cur.fetchone()[0] # type: ignore
result = await conn.fetchrow(
"""
select groupname
from picrinth.feeds
where id = $1
""",
feed_id,
)
if result is None:
return None
return result["groupname"] # type: ignore
+193 -223
View File
@@ -1,300 +1,270 @@
import psycopg2.extras
from psycopg2._psycopg import connection
from asyncpg import Connection
# group create and delete
def create_group(
conn: connection,
async def create_group(
conn: Connection,
groupname: str,
invite_code: str,
author: str,
allow_skips: bool = True,
feed_interval_minutes: int = 1440,
):
with conn.cursor() as cur:
cur.execute(
"""
insert into picrinth.groups
(groupname, invite_code, author, allow_skips,
feed_interval_minutes, created_at)
values (%s, %s, %s, %s, %s, now())
""",
(groupname, invite_code, author, allow_skips, feed_interval_minutes),
)
conn.commit()
return cur.rowcount > 0
result = await conn.execute(
"""
insert into picrinth.groups
(groupname, invite_code, author, allow_skips,
feed_interval_minutes, created_at)
values ($1, $2, $3, $4, $5, now())
""",
groupname, invite_code, author, allow_skips, feed_interval_minutes,
)
return result != "INSERT 0 0"
def delete_group(
conn: connection,
async def delete_group(
conn: Connection,
groupname: str
):
with conn.cursor() as cur:
cur.execute(
"""
delete from picrinth.groups
where groupname = %s
""",
(groupname,),
)
conn.commit()
return cur.rowcount > 0
result = await conn.execute(
"""
delete from picrinth.groups
where groupname = $1
""",
groupname,
)
return result != "DELETE 0"
# group checks
def check_group_existence(
conn: connection,
async def check_group_existence(
conn: Connection,
groupname: str
):
with conn.cursor() as cur:
cur.execute(
"""
select exists(
select 1
from picrinth.groups
where groupname = %s
);
""",
(groupname,),
)
return cur.fetchone()[0] # type: ignore
row = await conn.fetchrow(
"""
select exists(
select 1
from picrinth.groups
where groupname = $1
);
""",
groupname,
)
return row["exists"] # type: ignore
def check_invite_code(
conn: connection,
async def check_invite_code(
conn: Connection,
invite_code: str
):
with conn.cursor() as cur:
cur.execute(
"""
select exists(
select 1
from picrinth.groups
where invite_code = %s
);
""",
(invite_code,),
)
return cur.fetchone()[0] # type: ignore
row = await conn.fetchrow(
"""
select exists(
select 1
from picrinth.groups
where invite_code = $1
);
""",
invite_code,
)
return row["exists"] # type: ignore
def check_group_author(
conn: connection,
async def check_group_author(
conn: Connection,
groupname: str,
author: str
):
with conn.cursor() as cur:
cur.execute(
"""
select exists(
select 1
from picrinth.groups
where groupname = %s and author = %s
);
""",
(groupname, author),
)
return cur.fetchone()[0] # type: ignore
row = await conn.fetchrow(
"""
select exists(
select 1
from picrinth.groups
where groupname = $1 and author = $2
);
""",
groupname, author,
)
return row["exists"] # type: ignore
# group updates
def update_group_groupname(
conn: connection,
async def update_group_groupname(
conn: Connection,
groupname: str,
new_groupname: str
):
with conn.cursor() as cur:
cur.execute(
"""
update picrinth.groups
set groupname = %s
where groupname = %s
""",
(new_groupname, groupname),
)
conn.commit()
return cur.rowcount > 0
result = await conn.execute(
"""
update picrinth.groups
set groupname = $1
where groupname = $2
""",
new_groupname, groupname,
)
return result != "UPDATE 0"
def update_group_author(
conn: connection,
async def update_group_author(
conn: Connection,
groupname: str,
author: str
):
with conn.cursor() as cur:
cur.execute(
"""
update picrinth.groups
set author = %s
where groupname = %s
""",
(author, groupname),
)
conn.commit()
return cur.rowcount > 0
result = await conn.execute(
"""
update picrinth.groups
set author = $1
where groupname = $2
""",
author, groupname,
)
return result != "UPDATE 0"
def update_group_invite_code(
conn: connection,
async def update_group_invite_code(
conn: Connection,
groupname: str,
invite_code: str
):
with conn.cursor() as cur:
cur.execute(
"""
update picrinth.groups
set invite_code = %s
where groupname = %s
""",
(invite_code, groupname),
)
conn.commit()
return cur.rowcount > 0
result = await conn.execute(
"""
update picrinth.groups
set invite_code = $1
where groupname = $2
""",
invite_code, groupname,
)
return result != "UPDATE 0"
def update_group_allow_skips(
conn: connection,
async def update_group_allow_skips(
conn: Connection,
groupname: str,
allow_skips: bool
):
with conn.cursor() as cur:
cur.execute(
"""
update picrinth.groups
set allow_skips = %s
where groupname = %s
""",
(allow_skips, groupname),
)
conn.commit()
return cur.rowcount > 0
result = await conn.execute(
"""
update picrinth.groups
set allow_skips = $1
where groupname = $2
""",
allow_skips, groupname,
)
return result != "UPDATE 0"
def update_group_feed_interval(
conn: connection,
async def update_group_feed_interval(
conn: Connection,
groupname: str,
feed_interval: int
):
with conn.cursor() as cur:
cur.execute(
"""
update picrinth.groups
set feed_interval_minutes = %s
where groupname = %s
""",
(feed_interval, groupname),
)
conn.commit()
return cur.rowcount > 0
result = await conn.execute(
"""
update picrinth.groups
set feed_interval_minutes = $1
where groupname = $2
""",
feed_interval, groupname,
)
return result != "UPDATE 0"
def update_group_last_feed(
conn: connection,
async def update_group_last_feed(
conn: Connection,
groupname: str,
last_feed_id: int
):
with conn.cursor() as cur:
cur.execute(
"""
update picrinth.groups
set last_feed_id = %s
where groupname = %s
""",
(last_feed_id, groupname),
)
conn.commit()
return cur.rowcount > 0
result = await conn.execute(
"""
update picrinth.groups
set last_feed_id = $1
where groupname = $2
""",
last_feed_id, groupname,
)
return result != "UPDATE 0"
# group receiving
def get_group(
conn: connection,
async def get_group(
conn: Connection,
groupname: str
):
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
cur.execute(
"""
select groupname, author,
invite_code, allow_skips,
feed_interval_minutes,
last_feed_id, created_at
from picrinth.groups
where groupname = %s
""",
(groupname,),
)
return cur.fetchone()
return await conn.fetchrow(
"""
select groupname, author,
invite_code, allow_skips,
feed_interval_minutes,
last_feed_id, created_at
from picrinth.groups
where groupname = $1
""",
groupname,
)
def get_group_by_invite_code(
conn: connection,
async def get_group_by_invite_code(
conn: Connection,
invite_code: str
):
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
cur.execute(
"""
select groupname, author,
invite_code, allow_skips,
feed_interval_minutes,
last_feed_id, created_at
from picrinth.groups
where invite_code = %s
""",
(invite_code,),
)
return cur.fetchone()
return await conn.fetchrow(
"""
select groupname, author,
invite_code, allow_skips,
feed_interval_minutes,
last_feed_id, created_at
from picrinth.groups
where invite_code = $1
""",
invite_code,
)
def get_groupname_by_invite_code(
conn: connection,
async def get_groupname_by_invite_code(
conn: Connection,
invite_code: str
):
with conn.cursor() as cur:
cur.execute(
"""
select groupname
from picrinth.groups
where invite_code = %s
""",
(invite_code,),
)
result = cur.fetchone()
if result is None:
return None
return result[0] # type: ignore
result = await conn.fetchrow(
"""
select groupname
from picrinth.groups
where invite_code = $1
""",
invite_code,
)
if result is None:
return None
return result["groupname"] # type: ignore
def get_group_invite_code(
conn: connection,
async def get_group_invite_code(
conn: Connection,
groupname: str
):
with conn.cursor() as cur:
cur.execute(
"""
select invite_code
from picrinth.groups
where groupname = %s
""",
(groupname,),
)
result = cur.fetchone()
if result is None:
return None
return result[0] # type: ignore
result = await conn.fetchrow(
"""
select invite_code
from picrinth.groups
where groupname = $1
""",
groupname,
)
if result is None:
return None
return result["invite_code"] # type: ignore
def get_group_last_feed_id(
conn: connection,
async def get_group_last_feed_id(
conn: Connection,
groupname: str
):
with conn.cursor() as cur:
cur.execute(
"""
select last_feed_id
from picrinth.groups
where groupname = %s
""",
(groupname,),
)
result = cur.fetchone()
if result is None:
return None
return result[0] # type: ignore
result = await conn.fetchrow(
"""
select last_feed_id
from picrinth.groups
where groupname = $1
""",
groupname,
)
if result is None:
return None
return result["last_feed_id"] # type: ignore
+103
View File
@@ -0,0 +1,103 @@
import asyncpg
async def is_db_initialized(conn: asyncpg.Connection) -> bool:
row = await conn.fetchrow(
"""
select exists(
select 1
from information_schema.schemata
where schema_name = 'picrinth'
);
""",
)
return row["exists"] # type: ignore
async def initialize_db(conn: asyncpg.Connection) -> None:
await conn.execute(
"""
create schema picrinth;
create table picrinth.users (
id serial not null,
username text not null,
"password" text not null,
"role" text not null default 'user',
"disabled" bool not null default false,
last_seen_at timestamp with time zone null,
created_at timestamp with time zone default now(),
constraint username_unique unique (username)
);
create table picrinth.groups (
id serial not null,
groupname text not null,
invite_code text not null,
author text null,
allow_skips bool not null default true,
feed_interval_minutes integer null default 1440,
last_feed_id int null,
created_at timestamp with time zone default now(),
constraint groupname_unique unique (groupname),
constraint invite_code_unique unique (invite_code)
);
create table picrinth.memberships (
username text,
groupname text,
joined_at timestamp with time zone null,
primary key (username, groupname),
foreign key (username) references picrinth.users (username) on delete cascade on update cascade,
foreign key (groupname) references picrinth.groups (groupname) on delete cascade on update cascade
);
create table picrinth.pictures (
id serial not null,
source text not null,
external_id text not null,
"url" text not null,
metadata jsonb null,
created_at timestamp with time zone default now(),
constraint pictures_pkey primary key (id),
constraint url_unique unique (url)
);
create table picrinth.feeds (
id serial not null,
groupname text not null,
image_ids integer[] not null,
created_at timestamp with time zone default now(),
constraint feeds_pkey primary key (id),
foreign key (groupname) references picrinth.groups (groupname) on delete cascade on update cascade,
constraint unique_feed_per_timestamp_per_group unique (groupname, created_at)
);
create table picrinth.swipes (
id serial not null,
username text not null,
feed_id integer not null,
picture_id integer not null,
"value" smallint not null,
created_at timestamp with time zone default now(),
primary key (id),
foreign key (username) references picrinth.users (username) on delete cascade on update cascade,
foreign key (feed_id) references picrinth.feeds (id) on delete cascade on update cascade,
foreign key (picture_id) references picrinth.pictures (id) on delete cascade on update cascade,
constraint swipes_unique unique (username, feed_id, picture_id)
);
create table picrinth.accounts (
groupname text not null,
author text null,
platform text not null,
"login" text not null,
"password" text not null,
metadata jsonb null,
created_at timestamp with time zone default now(),
foreign key (groupname) references picrinth.groups (groupname) on delete cascade on update cascade,
-- foreign key (author) references picrinth.groups (author) on delete cascade on update cascade,
constraint unique_account_for_group_per_platform unique (groupname, platform)
);
""",
)
+32 -11
View File
@@ -1,17 +1,18 @@
import sys
import psycopg2
import asyncpg
from loguru import logger
import db.initialization
from db.models import database
from settings import startup_settings
def connect_db():
async def connect_db():
logger.info("Initializing DB connection")
try:
database.conn = psycopg2.connect(
dbname=startup_settings.db_name,
database.pool = await asyncpg.create_pool(
database=startup_settings.db_name,
user=startup_settings.db_user,
password=startup_settings.db_password,
host=startup_settings.db_host,
@@ -23,22 +24,42 @@ def connect_db():
logger.success("Successfully initialized DB connection")
def disconnect_db():
async def disconnect_db():
logger.info("Closing DB connection")
if database.conn:
if database.pool:
try:
database.conn.close()
await database.pool.close()
except Exception as e:
logger.error(f"Failed to disconnect from DB: {e}")
return
else:
logger.error("Failed to disconnect from DB: no connection")
logger.error("Failed to disconnect from DB: no connection pool")
logger.success("Successfully closed DB connection")
def get_db_connection():
if database.conn is not None:
yield database.conn
async def initialize_db():
if database.pool is None:
logger.error("No connection pool")
sys.exit(1)
async with database.pool.acquire() as conn:
try:
if await db.initialization.is_db_initialized(conn):
logger.success("DB already initialized, skipping setup")
return
else:
logger.info("Initializing DB schema and tables")
await db.initialization.initialize_db(conn)
except Exception as e:
logger.error(f"Failed to initialize DB: {e}")
sys.exit(1)
logger.success("Successfully initialized DB schema and tables")
async def get_db_connection():
if database.pool is not None:
async with database.pool.acquire() as connection:
yield connection
else:
logger.error("No connection pool")
sys.exit(1)
+55 -64
View File
@@ -1,92 +1,83 @@
from psycopg2._psycopg import connection
from asyncpg import Connection
# membership create and delete
def create_membership(
conn: connection,
async def create_membership(
conn: Connection,
username: str,
groupname: str
):
with conn.cursor() as cur:
cur.execute(
"""
insert into picrinth.memberships
(username, groupname, joined_at)
values (%s, %s, now())
""",
(username, groupname),
)
conn.commit()
return cur.rowcount > 0
result = await conn.execute(
"""
insert into picrinth.memberships
(username, groupname, joined_at)
values ($1, $2, now())
""",
username, groupname,
)
return result != "INSERT 0 0"
def delete_membership(
conn: connection,
async def delete_membership(
conn: Connection,
username: str,
groupname: str
):
with conn.cursor() as cur:
cur.execute(
"""
delete from picrinth.memberships
where username = %s and groupname = %s
""",
(username, groupname),
)
conn.commit()
return cur.rowcount > 0
result = await conn.execute(
"""
delete from picrinth.memberships
where username = $1 and groupname = $2
""",
username, groupname,
)
return result != "DELETE 0"
# membership checks
def check_membership_exists(
conn: connection,
async def check_membership_exists(
conn: Connection,
username: str,
groupname: str
):
with conn.cursor() as cur:
cur.execute(
"""
select exists(
select 1
from picrinth.memberships
where username = %s and groupname = %s
);
""",
(username, groupname),
)
return cur.fetchone()[0] # type: ignore
row = await conn.fetchrow(
"""
select exists(
select 1
from picrinth.memberships
where username = $1 and groupname = $2
);
""",
username, groupname,
)
return row["exists"] # type: ignore
# membership receiving
def get_memberships_by_username(
conn: connection,
async def get_memberships_by_username(
conn: Connection,
username: str
):
with conn.cursor() as cur:
cur.execute(
"""
select *
from picrinth.memberships
where username = %s
""",
(username,),
)
return cur.fetchall()
return await conn.fetch(
"""
select *
from picrinth.memberships
where username = $1
""",
username,
)
def get_memberships_by_groupname(
conn: connection,
async def get_memberships_by_groupname(
conn: Connection,
groupname: str
):
with conn.cursor() as cur:
cur.execute(
"""
select *
from picrinth.memberships
where groupname = %s
""",
(groupname,),
)
return cur.fetchall()
return await conn.fetch(
"""
select *
from picrinth.memberships
where groupname = $1
""",
groupname,
)
+4 -2
View File
@@ -1,8 +1,10 @@
from psycopg2._psycopg import connection
import asyncpg
from asyncpg import Pool
class DataBase:
conn: connection | None = None
pool: Pool | None = None
database = DataBase()
+46 -55
View File
@@ -1,78 +1,69 @@
import json
import psycopg2.extras
from psycopg2._psycopg import connection
from asyncpg import Connection
# picture create and delete
def create_picture(
conn: connection,
async def create_picture(
conn: Connection,
source: str,
external_id: str,
url: str,
metadata: dict
):
with conn.cursor() as cur:
cur.execute(
"""
insert into picrinth.pictures
(source, external_id, url, metadata, created_at)
values (%s, %s, %s, %s, now())
returning id
""",
(source, external_id, url, json.dumps(metadata)),
)
result = cur.fetchone()
conn.commit()
if result is None:
return None
return result[0]
result = await conn.fetchrow(
"""
insert into picrinth.pictures
(source, external_id, url, metadata, created_at)
values ($1, $2, $3, $4, now())
returning id
""",
source, external_id, url, json.dumps(metadata),
)
if result is None:
return None
return result["id"]
def delete_picture(
conn: connection,
async def delete_picture(
conn: Connection,
id: int
):
with conn.cursor() as cur:
cur.execute(
"""
delete from picrinth.pictures
where id = %s
""",
(id,),
)
conn.commit()
return cur.rowcount > 0
result = await conn.execute(
"""
delete from picrinth.pictures
where id = $1
""",
id,
)
return result != "DELETE 0"
# picture receiving
def get_picture(
conn: connection,
async def get_picture(
conn: Connection,
id: int
):
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
cur.execute(
"""
select id, source,
external_id, url,
metadata, created_at
from picrinth.pictures
where id = %s
""",
(id,),
)
return cur.fetchone()
return await conn.fetchrow(
"""
select id, source,
external_id, url,
metadata, created_at
from picrinth.pictures
where id = $1
""",
id,
)
def get_all_pictures_ids(
conn: connection,
async def get_all_pictures_ids(
conn: Connection,
):
with conn.cursor() as cur:
cur.execute(
"""
select id
from picrinth.pictures
""",
)
return [element for (element,) in cur.fetchall()]
rows = await conn.fetch(
"""
select id
from picrinth.pictures
""",
)
return [element["id"] for element in rows]
+57 -69
View File
@@ -1,100 +1,88 @@
import psycopg2.extras
from psycopg2._psycopg import connection
from asyncpg import Connection
# swipe create and delete
def create_swipe(
conn: connection,
async def create_swipe(
conn: Connection,
username: str,
feed_id: int,
picture_id: int,
value: int
):
with conn.cursor() as cur:
cur.execute(
"""
insert into picrinth.swipes
(username, feed_id, picture_id, value, created_at)
values (%s, %s, %s, %s, now())
returning id
""",
(username, feed_id, picture_id, value),
)
result = cur.fetchone()
conn.commit()
if result is None:
return None
return result[0]
result = await conn.fetchrow(
"""
insert into picrinth.swipes
(username, feed_id, picture_id, value, created_at)
values ($1, $2, $3, $4, now())
returning id
""",
username, feed_id, picture_id, value,
)
if result is None:
return None
return result["id"]
def delete_swipe(
conn: connection,
async def delete_swipe(
conn: Connection,
username: str,
feed_id: int,
picture_id: int
):
with conn.cursor() as cur:
cur.execute(
"""
delete from picrinth.swipes
where username = %s and picture_id = %s and feed_id = %s
""",
(username, picture_id, feed_id),
)
conn.commit()
return cur.rowcount > 0
result = await conn.execute(
"""
delete from picrinth.swipes
where username = $1 and picture_id = $2 and feed_id = $3
""",
username, picture_id, feed_id,
)
return result != "DELETE 0"
# swipe receiving
def get_swipe(
conn: connection,
async def get_swipe(
conn: Connection,
username: str,
feed_id: int,
picture_id: int
):
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
cur.execute(
"""
select username,
feed_id, picture_id,
value, created_at
from picrinth.swipes
where username = %s and feed_id = %s and picture_id = %s
""",
(username, feed_id, picture_id),
)
return cur.fetchone()
return await conn.fetchrow(
"""
select username,
feed_id, picture_id,
value, created_at
from picrinth.swipes
where username = $1 and feed_id = $2 and picture_id = $3
""",
username, feed_id, picture_id,
)
def get_swipes_by_picture_id(
conn: connection,
async def get_swipes_by_picture_id(
conn: Connection,
feed_id: int,
picture_id: int
):
with conn.cursor() as cur:
cur.execute(
"""
select *
from picrinth.swipes
where feed_id = %s and picture_id = %s
""",
(feed_id, picture_id),
)
return cur.fetchone()
return await conn.fetchrow(
"""
select *
from picrinth.swipes
where feed_id = $1 and picture_id = $2
""",
feed_id, picture_id,
)
def get_swipes_by_user(
conn: connection,
async def get_swipes_by_user(
conn: Connection,
username: str,
feed_id: int
):
with conn.cursor() as cur:
cur.execute(
"""
select *
from picrinth.swipes
where username = %s and feed_id = %s
""",
(username, feed_id),
)
return cur.fetchone()
return await conn.fetchrow(
"""
select *
from picrinth.swipes
where username = $1 and feed_id = $2
""",
username, feed_id,
)
+129 -151
View File
@@ -1,203 +1,181 @@
import psycopg2.extras
from psycopg2._psycopg import connection
from asyncpg import Connection
# user create and delete
def create_user(
conn: connection,
async def create_user(
conn: Connection,
username: str,
password: str,
role: str = "user"
):
with conn.cursor() as cur:
cur.execute(
"""
insert into picrinth.users
(username, password, role, disabled, created_at)
values (%s, %s, %s, false, now())
""",
(username, password, role),
)
conn.commit()
return cur.rowcount > 0
result = await conn.execute(
"""
insert into picrinth.users
(username, password, role, disabled, created_at)
values ($1, $2, $3, false, now())
""",
username, password, role,
)
return result != "INSERT 0 0"
def delete_user(
conn: connection,
async def delete_user(
conn: Connection,
username: str
):
with conn.cursor() as cur:
cur.execute(
"""
delete from picrinth.users
where username = %s
""",
(username,),
)
conn.commit()
return cur.rowcount > 0
result = await conn.execute(
"""
delete from picrinth.users
where username = $1
""",
username,
)
return result != "DELETE 0"
# user checks
def check_user_existence(
conn: connection,
async def check_user_existence(
conn: Connection,
username: str
):
with conn.cursor() as cur:
cur.execute(
"""
select exists(
select 1
from picrinth.users
where username = %s
);
""",
(username,),
)
return cur.fetchone()[0] # type: ignore
def check_user_disabled(
conn: connection,
username: str
):
with conn.cursor() as cur:
cur.execute(
"""
select disabled
row = await conn.fetchrow(
"""
select exists(
select 1
from picrinth.users
where username = %s;
""",
(username,),
)
result = cur.fetchone()
if result is None:
return None
return result[0] # type: ignore
where username = $1
);
""",
username,
)
return row["exists"] # type: ignore
async def check_user_disabled(
conn: Connection,
username: str
):
result = await conn.fetchrow(
"""
select disabled
from picrinth.users
where username = $1;
""",
username,
)
if result is None:
return None
return result["disabled"] # type: ignore
# user updates
def update_user_disabled(
conn: connection,
async def update_user_disabled(
conn: Connection,
username: str,
disabled: bool
):
# if disabled = True => user is disabled
with conn.cursor() as cur:
cur.execute(
"""
update picrinth.users
set disabled = %s
where username = %s
""",
(disabled, username),
)
conn.commit()
return cur.rowcount > 0
result = await conn.execute(
"""
update picrinth.users
set disabled = $1
where username = $2
""",
disabled, username,
)
return result != "UPDATE 0"
def update_user_role(
conn: connection,
async def update_user_role(
conn: Connection,
username: str,
role: str
):
with conn.cursor() as cur:
cur.execute(
"""
update picrinth.users
set role = %s
where username = %s
""",
(role, username),
)
conn.commit()
return cur.rowcount > 0
result = await conn.execute(
"""
update picrinth.users
set role = $1
where username = $2
""",
role, username,
)
return result != "UPDATE 0"
def update_user_username(
conn: connection,
async def update_user_username(
conn: Connection,
username: str,
newUsername: str
):
with conn.cursor() as cur:
cur.execute(
"""
update picrinth.users
set username = %s
where username = %s;
""",
(newUsername, username),
)
conn.commit()
return cur.rowcount > 0
result = await conn.execute(
"""
update picrinth.users
set username = $1
where username = $2;
""",
newUsername, username,
)
return result != "UPDATE 0"
def update_user_password(
conn: connection,
async def update_user_password(
conn: Connection,
username: str,
password: str
):
with conn.cursor() as cur:
cur.execute(
"""
update picrinth.users
set password = %s
where username = %s
""",
(password, username),
)
conn.commit()
return cur.rowcount > 0
result = await conn.execute(
"""
update picrinth.users
set password = $1
where username = $2
""",
password, username,
)
return result != "UPDATE 0"
def update_user_last_seen(
conn: connection,
async def update_user_last_seen(
conn: Connection,
username: str
):
with conn.cursor() as cur:
cur.execute(
"""
update picrinth.users
set last_seen_at = now()
where username = %s
""",
(username,),
)
conn.commit()
return cur.rowcount > 0
result = await conn.execute(
"""
update picrinth.users
set last_seen_at = now()
where username = $1
""",
username,
)
return result != "UPDATE 0"
# user receiving
def get_user(
conn: connection,
async def get_user(
conn: Connection,
username: str
):
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cur:
cur.execute(
"""
select username, password, role,
disabled, last_seen_at, created_at
from picrinth.users
where username = %s
""",
(username,),
)
return cur.fetchone()
return await conn.fetchrow(
"""
select username, password, role,
disabled, last_seen_at, created_at
from picrinth.users
where username = $1
""",
username,
)
def get_user_password(
conn: connection,
async def get_user_password(
conn: Connection,
username: str
):
with conn.cursor() as cur:
cur.execute(
"""
select password
from picrinth.users
where username = %s
""",
(username,),
)
result = cur.fetchone()
if result is None:
return None
return result[0] # type: ignore
result = await conn.fetchrow(
"""
select password
from picrinth.users
where username = $1
""",
username,
)
if result is None:
return None
return result["password"] # type: ignore
+3 -3
View File
@@ -1,10 +1,10 @@
from psycopg2._psycopg import connection
from asyncpg import Connection
import db.pictures as db
from api.models import Account, Picture
def gelbooru(conn: connection, account: Account):
async def gelbooru(conn: Connection, account: Account):
picture = Picture(external_id = "3", url = "", metadata = {})
picture_id = db.create_picture(conn, "gelbooru", picture.external_id, picture.url, picture.metadata)
picture_id = await db.create_picture(conn, "gelbooru", picture.external_id, picture.url, picture.metadata)
return picture_id
+3 -3
View File
@@ -1,10 +1,10 @@
from psycopg2._psycopg import connection
from asyncpg import Connection
import db.pictures as db
from api.models import Account, Picture
def pinterest(conn: connection, account: Account):
async def pinterest(conn: Connection, account: Account):
picture = Picture(external_id = "1", url = "", metadata = {})
picture_id = db.create_picture(conn, "pinterest", picture.external_id, picture.url, picture.metadata)
picture_id = await db.create_picture(conn, "pinterest", picture.external_id, picture.url, picture.metadata)
return picture_id
+6 -6
View File
@@ -1,8 +1,8 @@
from asyncpg import Connection
from fastapi import HTTPException, status
from gppt import GetPixivToken
from loguru import logger
from pixivpy3 import AppPixivAPI
from psycopg2._psycopg import connection
import db.pictures as db
from api.models import Account, Picture
@@ -10,14 +10,14 @@ from db.accounts import update_account_metadata
# Wrapper functions
def pixiv(conn: connection, account: Account) -> list[int]:
async def pixiv(conn: Connection, account: Account) -> list[int]:
# Getting refresh token
refresh_token = account.metadata.get('refresh_token', '')
if not refresh_token:
try:
refresh_token = get_refresh_token(account.login, account.password)
account.metadata['refresh_token'] = refresh_token
update_account_metadata(conn, account.groupname, account.platform, account.metadata)
await update_account_metadata(conn, account.groupname, account.platform, account.metadata)
except Exception as e:
# TODO: review ruff "do not use bare `except`"
logger.debug(f"Pixiv refresh token missing and creation failed: {e}")
@@ -50,18 +50,18 @@ def pixiv(conn: connection, account: Account) -> list[int]:
# Saving recommendations as Pictures to DB
pictures_ids = []
for picture in pictures:
pictures_ids.append(db.create_picture(conn, "pixiv", picture.external_id, picture.url, picture.metadata))
pictures_ids.append(await db.create_picture(conn, "pixiv", picture.external_id, picture.url, picture.metadata))
return pictures_ids
def like_picture(conn: connection, account: Account, picture_id: int):
async def like_picture(conn: Connection, account: Account, picture_id: int):
# Getting refresh token
refresh_token = account.metadata.get('refresh_token', '')
if not refresh_token:
try:
refresh_token = get_refresh_token(account.login, account.password)
account.metadata['refresh_token'] = refresh_token
update_account_metadata(conn, account.groupname, account.platform, account.metadata)
await update_account_metadata(conn, account.groupname, account.platform, account.metadata)
except Exception as e:
# TODO: review ruff "do not use bare `except`"
logger.debug(f"Pixiv refresh token missing and creation failed: {e}")
+10 -10
View File
@@ -1,5 +1,5 @@
from asyncpg import Connection
from loguru import logger
from psycopg2._psycopg import connection
import db.pictures as db
from api.models import Account, Picture
@@ -9,8 +9,8 @@ from scraper.pixiv import pixiv
from settings import startup_settings
def generate_feed(
conn: connection,
async def generate_feed(
conn: Connection,
accounts: list[Account]
) -> list:
feed = []
@@ -20,22 +20,22 @@ def generate_feed(
match account.platform:
case "pinterest":
temp_feed = pinterest(conn, account)
temp_feed = await pinterest(conn, account)
feed.append(temp_feed)
case "pixiv":
temp_feed = pixiv(conn, account)
temp_feed = await pixiv(conn, account)
feed.append(temp_feed)
case "gelbooru":
temp_feed = gelbooru(conn, account)
temp_feed = await gelbooru(conn, account)
feed.append(temp_feed)
case _:
logger.warning(f"Platform for feed generation is not supported: {account.platform}")
# TODO: remove mock results
feed = db.get_all_pictures_ids(conn)
feed = await db.get_all_pictures_ids(conn)
return feed
@@ -43,11 +43,11 @@ def generate_feed(
def process_swipe():
return
def get_picture(
conn: connection,
async def get_picture(
conn: Connection,
picture_id: int
) -> int:
picture_data = db.get_picture(conn, picture_id)
picture_data = await db.get_picture(conn, picture_id)
if picture_id is None:
return -1
return Picture().fill(picture_data).id
+1 -1
View File
@@ -41,7 +41,7 @@ def settings_up():
with open(json_path + json_settings_name, "w") as f:
json.dump(settings.model_dump_json(), f, ensure_ascii = False, indent=4)
logger.info("Wrote settings to the JSON")
logger.info("Successfully configured settings")
logger.success("Successfully configured settings")
except Exception as e:
logger.error(f"Failed to configure settings during startup: {e}")
raise e
Generated
+658
View File
@@ -0,0 +1,658 @@
version = 1
revision = 3
requires-python = ">=3.13"
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
]
[[package]]
name = "anyio"
version = "4.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" },
]
[[package]]
name = "asyncpg"
version = "0.30.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/2f/4c/7c991e080e106d854809030d8584e15b2e996e26f16aee6d757e387bc17d/asyncpg-0.30.0.tar.gz", hash = "sha256:c551e9928ab6707602f44811817f82ba3c446e018bfe1d3abecc8ba5f3eac851", size = 957746, upload-time = "2024-10-20T00:30:41.127Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3a/22/e20602e1218dc07692acf70d5b902be820168d6282e69ef0d3cb920dc36f/asyncpg-0.30.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05b185ebb8083c8568ea8a40e896d5f7af4b8554b64d7719c0eaa1eb5a5c3a70", size = 670373, upload-time = "2024-10-20T00:29:55.165Z" },
{ url = "https://files.pythonhosted.org/packages/3d/b3/0cf269a9d647852a95c06eb00b815d0b95a4eb4b55aa2d6ba680971733b9/asyncpg-0.30.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c47806b1a8cbb0a0db896f4cd34d89942effe353a5035c62734ab13b9f938da3", size = 634745, upload-time = "2024-10-20T00:29:57.14Z" },
{ url = "https://files.pythonhosted.org/packages/8e/6d/a4f31bf358ce8491d2a31bfe0d7bcf25269e80481e49de4d8616c4295a34/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b6fde867a74e8c76c71e2f64f80c64c0f3163e687f1763cfaf21633ec24ec33", size = 3512103, upload-time = "2024-10-20T00:29:58.499Z" },
{ url = "https://files.pythonhosted.org/packages/96/19/139227a6e67f407b9c386cb594d9628c6c78c9024f26df87c912fabd4368/asyncpg-0.30.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46973045b567972128a27d40001124fbc821c87a6cade040cfcd4fa8a30bcdc4", size = 3592471, upload-time = "2024-10-20T00:30:00.354Z" },
{ url = "https://files.pythonhosted.org/packages/67/e4/ab3ca38f628f53f0fd28d3ff20edff1c975dd1cb22482e0061916b4b9a74/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9110df111cabc2ed81aad2f35394a00cadf4f2e0635603db6ebbd0fc896f46a4", size = 3496253, upload-time = "2024-10-20T00:30:02.794Z" },
{ url = "https://files.pythonhosted.org/packages/ef/5f/0bf65511d4eeac3a1f41c54034a492515a707c6edbc642174ae79034d3ba/asyncpg-0.30.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04ff0785ae7eed6cc138e73fc67b8e51d54ee7a3ce9b63666ce55a0bf095f7ba", size = 3662720, upload-time = "2024-10-20T00:30:04.501Z" },
{ url = "https://files.pythonhosted.org/packages/e7/31/1513d5a6412b98052c3ed9158d783b1e09d0910f51fbe0e05f56cc370bc4/asyncpg-0.30.0-cp313-cp313-win32.whl", hash = "sha256:ae374585f51c2b444510cdf3595b97ece4f233fde739aa14b50e0d64e8a7a590", size = 560404, upload-time = "2024-10-20T00:30:06.537Z" },
{ url = "https://files.pythonhosted.org/packages/c8/a4/cec76b3389c4c5ff66301cd100fe88c318563ec8a520e0b2e792b5b84972/asyncpg-0.30.0-cp313-cp313-win_amd64.whl", hash = "sha256:f59b430b8e27557c3fb9869222559f7417ced18688375825f8f12302c34e915e", size = 621623, upload-time = "2024-10-20T00:30:09.024Z" },
]
[[package]]
name = "attrs"
version = "25.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" },
]
[[package]]
name = "bcrypt"
version = "4.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bb/5d/6d7433e0f3cd46ce0b43cd65e1db465ea024dbb8216fb2404e919c2ad77b/bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18", size = 25697, upload-time = "2025-02-28T01:24:09.174Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/2c/3d44e853d1fe969d229bd58d39ae6902b3d924af0e2b5a60d17d4b809ded/bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281", size = 483719, upload-time = "2025-02-28T01:22:34.539Z" },
{ url = "https://files.pythonhosted.org/packages/a1/e2/58ff6e2a22eca2e2cff5370ae56dba29d70b1ea6fc08ee9115c3ae367795/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb", size = 272001, upload-time = "2025-02-28T01:22:38.078Z" },
{ url = "https://files.pythonhosted.org/packages/37/1f/c55ed8dbe994b1d088309e366749633c9eb90d139af3c0a50c102ba68a1a/bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180", size = 277451, upload-time = "2025-02-28T01:22:40.787Z" },
{ url = "https://files.pythonhosted.org/packages/d7/1c/794feb2ecf22fe73dcfb697ea7057f632061faceb7dcf0f155f3443b4d79/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f", size = 272792, upload-time = "2025-02-28T01:22:43.144Z" },
{ url = "https://files.pythonhosted.org/packages/13/b7/0b289506a3f3598c2ae2bdfa0ea66969812ed200264e3f61df77753eee6d/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09", size = 289752, upload-time = "2025-02-28T01:22:45.56Z" },
{ url = "https://files.pythonhosted.org/packages/dc/24/d0fb023788afe9e83cc118895a9f6c57e1044e7e1672f045e46733421fe6/bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d", size = 277762, upload-time = "2025-02-28T01:22:47.023Z" },
{ url = "https://files.pythonhosted.org/packages/e4/38/cde58089492e55ac4ef6c49fea7027600c84fd23f7520c62118c03b4625e/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd", size = 272384, upload-time = "2025-02-28T01:22:49.221Z" },
{ url = "https://files.pythonhosted.org/packages/de/6a/d5026520843490cfc8135d03012a413e4532a400e471e6188b01b2de853f/bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af", size = 277329, upload-time = "2025-02-28T01:22:51.603Z" },
{ url = "https://files.pythonhosted.org/packages/b3/a3/4fc5255e60486466c389e28c12579d2829b28a527360e9430b4041df4cf9/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231", size = 305241, upload-time = "2025-02-28T01:22:53.283Z" },
{ url = "https://files.pythonhosted.org/packages/c7/15/2b37bc07d6ce27cc94e5b10fd5058900eb8fb11642300e932c8c82e25c4a/bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c", size = 309617, upload-time = "2025-02-28T01:22:55.461Z" },
{ url = "https://files.pythonhosted.org/packages/5f/1f/99f65edb09e6c935232ba0430c8c13bb98cb3194b6d636e61d93fe60ac59/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f", size = 335751, upload-time = "2025-02-28T01:22:57.81Z" },
{ url = "https://files.pythonhosted.org/packages/00/1b/b324030c706711c99769988fcb694b3cb23f247ad39a7823a78e361bdbb8/bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d", size = 355965, upload-time = "2025-02-28T01:22:59.181Z" },
{ url = "https://files.pythonhosted.org/packages/aa/dd/20372a0579dd915dfc3b1cd4943b3bca431866fcb1dfdfd7518c3caddea6/bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4", size = 155316, upload-time = "2025-02-28T01:23:00.763Z" },
{ url = "https://files.pythonhosted.org/packages/6d/52/45d969fcff6b5577c2bf17098dc36269b4c02197d551371c023130c0f890/bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669", size = 147752, upload-time = "2025-02-28T01:23:02.908Z" },
{ url = "https://files.pythonhosted.org/packages/11/22/5ada0b9af72b60cbc4c9a399fdde4af0feaa609d27eb0adc61607997a3fa/bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d", size = 498019, upload-time = "2025-02-28T01:23:05.838Z" },
{ url = "https://files.pythonhosted.org/packages/b8/8c/252a1edc598dc1ce57905be173328eda073083826955ee3c97c7ff5ba584/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b", size = 279174, upload-time = "2025-02-28T01:23:07.274Z" },
{ url = "https://files.pythonhosted.org/packages/29/5b/4547d5c49b85f0337c13929f2ccbe08b7283069eea3550a457914fc078aa/bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e", size = 283870, upload-time = "2025-02-28T01:23:09.151Z" },
{ url = "https://files.pythonhosted.org/packages/be/21/7dbaf3fa1745cb63f776bb046e481fbababd7d344c5324eab47f5ca92dd2/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59", size = 279601, upload-time = "2025-02-28T01:23:11.461Z" },
{ url = "https://files.pythonhosted.org/packages/6d/64/e042fc8262e971347d9230d9abbe70d68b0a549acd8611c83cebd3eaec67/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753", size = 297660, upload-time = "2025-02-28T01:23:12.989Z" },
{ url = "https://files.pythonhosted.org/packages/50/b8/6294eb84a3fef3b67c69b4470fcdd5326676806bf2519cda79331ab3c3a9/bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761", size = 284083, upload-time = "2025-02-28T01:23:14.5Z" },
{ url = "https://files.pythonhosted.org/packages/62/e6/baff635a4f2c42e8788fe1b1633911c38551ecca9a749d1052d296329da6/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb", size = 279237, upload-time = "2025-02-28T01:23:16.686Z" },
{ url = "https://files.pythonhosted.org/packages/39/48/46f623f1b0c7dc2e5de0b8af5e6f5ac4cc26408ac33f3d424e5ad8da4a90/bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d", size = 283737, upload-time = "2025-02-28T01:23:18.897Z" },
{ url = "https://files.pythonhosted.org/packages/49/8b/70671c3ce9c0fca4a6cc3cc6ccbaa7e948875a2e62cbd146e04a4011899c/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f", size = 312741, upload-time = "2025-02-28T01:23:21.041Z" },
{ url = "https://files.pythonhosted.org/packages/27/fb/910d3a1caa2d249b6040a5caf9f9866c52114d51523ac2fb47578a27faee/bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732", size = 316472, upload-time = "2025-02-28T01:23:23.183Z" },
{ url = "https://files.pythonhosted.org/packages/dc/cf/7cf3a05b66ce466cfb575dbbda39718d45a609daa78500f57fa9f36fa3c0/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef", size = 343606, upload-time = "2025-02-28T01:23:25.361Z" },
{ url = "https://files.pythonhosted.org/packages/e3/b8/e970ecc6d7e355c0d892b7f733480f4aa8509f99b33e71550242cf0b7e63/bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304", size = 362867, upload-time = "2025-02-28T01:23:26.875Z" },
{ url = "https://files.pythonhosted.org/packages/a9/97/8d3118efd8354c555a3422d544163f40d9f236be5b96c714086463f11699/bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51", size = 160589, upload-time = "2025-02-28T01:23:28.381Z" },
{ url = "https://files.pythonhosted.org/packages/29/07/416f0b99f7f3997c69815365babbc2e8754181a4b1899d921b3c7d5b6f12/bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62", size = 152794, upload-time = "2025-02-28T01:23:30.187Z" },
{ url = "https://files.pythonhosted.org/packages/6e/c1/3fa0e9e4e0bfd3fd77eb8b52ec198fd6e1fd7e9402052e43f23483f956dd/bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3", size = 498969, upload-time = "2025-02-28T01:23:31.945Z" },
{ url = "https://files.pythonhosted.org/packages/ce/d4/755ce19b6743394787fbd7dff6bf271b27ee9b5912a97242e3caf125885b/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24", size = 279158, upload-time = "2025-02-28T01:23:34.161Z" },
{ url = "https://files.pythonhosted.org/packages/9b/5d/805ef1a749c965c46b28285dfb5cd272a7ed9fa971f970435a5133250182/bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef", size = 284285, upload-time = "2025-02-28T01:23:35.765Z" },
{ url = "https://files.pythonhosted.org/packages/ab/2b/698580547a4a4988e415721b71eb45e80c879f0fb04a62da131f45987b96/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b", size = 279583, upload-time = "2025-02-28T01:23:38.021Z" },
{ url = "https://files.pythonhosted.org/packages/f2/87/62e1e426418204db520f955ffd06f1efd389feca893dad7095bf35612eec/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676", size = 297896, upload-time = "2025-02-28T01:23:39.575Z" },
{ url = "https://files.pythonhosted.org/packages/cb/c6/8fedca4c2ada1b6e889c52d2943b2f968d3427e5d65f595620ec4c06fa2f/bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1", size = 284492, upload-time = "2025-02-28T01:23:40.901Z" },
{ url = "https://files.pythonhosted.org/packages/4d/4d/c43332dcaaddb7710a8ff5269fcccba97ed3c85987ddaa808db084267b9a/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe", size = 279213, upload-time = "2025-02-28T01:23:42.653Z" },
{ url = "https://files.pythonhosted.org/packages/dc/7f/1e36379e169a7df3a14a1c160a49b7b918600a6008de43ff20d479e6f4b5/bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0", size = 284162, upload-time = "2025-02-28T01:23:43.964Z" },
{ url = "https://files.pythonhosted.org/packages/1c/0a/644b2731194b0d7646f3210dc4d80c7fee3ecb3a1f791a6e0ae6bb8684e3/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f", size = 312856, upload-time = "2025-02-28T01:23:46.011Z" },
{ url = "https://files.pythonhosted.org/packages/dc/62/2a871837c0bb6ab0c9a88bf54de0fc021a6a08832d4ea313ed92a669d437/bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23", size = 316726, upload-time = "2025-02-28T01:23:47.575Z" },
{ url = "https://files.pythonhosted.org/packages/0c/a1/9898ea3faac0b156d457fd73a3cb9c2855c6fd063e44b8522925cdd8ce46/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe", size = 343664, upload-time = "2025-02-28T01:23:49.059Z" },
{ url = "https://files.pythonhosted.org/packages/40/f2/71b4ed65ce38982ecdda0ff20c3ad1b15e71949c78b2c053df53629ce940/bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505", size = 363128, upload-time = "2025-02-28T01:23:50.399Z" },
{ url = "https://files.pythonhosted.org/packages/11/99/12f6a58eca6dea4be992d6c681b7ec9410a1d9f5cf368c61437e31daa879/bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a", size = 160598, upload-time = "2025-02-28T01:23:51.775Z" },
{ url = "https://files.pythonhosted.org/packages/a9/cf/45fb5261ece3e6b9817d3d82b2f343a505fd58674a92577923bc500bd1aa/bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b", size = 152799, upload-time = "2025-02-28T01:23:53.139Z" },
]
[[package]]
name = "certifi"
version = "2025.8.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" },
]
[[package]]
name = "cffi"
version = "2.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser", marker = "implementation_name != 'PyPy'" },
]
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 = [
{ 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/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
{ url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
{ url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
{ url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
{ url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.4.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" },
{ url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" },
{ url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" },
{ url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" },
{ url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" },
{ url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" },
{ url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" },
{ url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" },
{ url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" },
{ url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" },
{ url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" },
{ url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" },
{ url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" },
{ url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" },
]
[[package]]
name = "click"
version = "8.2.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/60/6c/8ca2efa64cf75a977a0d7fac081354553ebe483345c734fb6b6515d96bbc/click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202", size = 286342, upload-time = "2025-05-20T23:19:49.832Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/85/32/10bb5764d90a8eee674e9dc6f4db6a0ab47c8c4d0d83c27f7c39ac415a4d/click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b", size = 102215, upload-time = "2025-05-20T23:19:47.796Z" },
]
[[package]]
name = "cloudscraper"
version = "1.2.71"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyparsing" },
{ name = "requests" },
{ name = "requests-toolbelt" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ac/25/6d0481860583f44953bd791de0b7c4f6d7ead7223f8a17e776247b34a5b4/cloudscraper-1.2.71.tar.gz", hash = "sha256:429c6e8aa6916d5bad5c8a5eac50f3ea53c9ac22616f6cb21b18dcc71517d0d3", size = 93261, upload-time = "2023-04-25T23:20:19.467Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/81/97/fc88803a451029688dffd7eb446dc1b529657577aec13aceff1cc9628c5d/cloudscraper-1.2.71-py2.py3-none-any.whl", hash = "sha256:76f50ca529ed2279e220837befdec892626f9511708e200d48d5bb76ded679b0", size = 99652, upload-time = "2023-04-25T23:20:15.974Z" },
]
[[package]]
name = "colorama"
version = "0.4.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
]
[[package]]
name = "fastapi"
version = "0.116.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pydantic" },
{ name = "starlette" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/78/d7/6c8b3bfe33eeffa208183ec037fee0cce9f7f024089ab1c5d12ef04bd27c/fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143", size = 296485, upload-time = "2025-07-11T16:22:32.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631, upload-time = "2025-07-11T16:22:30.485Z" },
]
[[package]]
name = "gppt"
version = "4.1.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pixivpy3" },
{ name = "pwinput" },
{ name = "requests" },
{ name = "selenium" },
]
sdist = { url = "https://files.pythonhosted.org/packages/4c/02/e4031ccf92deb123a28418a6cdf40bdbbb7a3e5e0240717bcea3d3752aef/gppt-4.1.1.tar.gz", hash = "sha256:f43cc59f68f799ffe54b0718a4706487541888bf066926479f7fc6160caffb2f", size = 10935, upload-time = "2025-04-08T13:38:26.787Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b9/c1/648e7727c06c26becd83016db08716fb93ffeb578f981229a560f7f9ab99/gppt-4.1.1-py3-none-any.whl", hash = "sha256:5a8b8f6e29d4ccc8555d715841a768d3e6cbac3be690742dbc6ad0949fb8990d", size = 12344, upload-time = "2025-04-08T13:38:25.815Z" },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" },
]
[[package]]
name = "loguru"
version = "0.7.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
{ name = "win32-setctime", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" },
]
[[package]]
name = "outcome"
version = "1.3.0.post0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/df/77698abfac98571e65ffeb0c1fba8ffd692ab8458d617a0eed7d9a8d38f2/outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8", size = 21060, upload-time = "2023-10-26T04:26:04.361Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/55/8b/5ab7257531a5d830fc8000c476e63c935488d74609b50f9384a643ec0a62/outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b", size = 10692, upload-time = "2023-10-26T04:26:02.532Z" },
]
[[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]]
name = "picrinth-server"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "annotated-types" },
{ name = "anyio" },
{ name = "asyncpg" },
{ name = "attrs" },
{ name = "bcrypt" },
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "click" },
{ name = "cloudscraper" },
{ name = "fastapi" },
{ name = "gppt" },
{ name = "h11" },
{ name = "idna" },
{ name = "loguru" },
{ name = "outcome" },
{ name = "passlib" },
{ name = "pixivpy3" },
{ name = "pwinput" },
{ name = "pydantic" },
{ name = "pydantic-core" },
{ name = "pyjwt" },
{ name = "pyparsing" },
{ name = "pysocks" },
{ name = "python-decouple" },
{ name = "python-multipart" },
{ name = "requests" },
{ name = "requests-toolbelt" },
{ name = "selenium" },
{ name = "sniffio" },
{ name = "sortedcontainers" },
{ name = "starlette" },
{ name = "trio" },
{ name = "trio-websocket" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
{ name = "urllib3" },
{ name = "uvicorn" },
{ name = "websocket-client" },
{ name = "wsproto" },
]
[package.metadata]
requires-dist = [
{ name = "annotated-types", specifier = "==0.7.0" },
{ name = "anyio", specifier = "==4.9.0" },
{ name = "asyncpg", specifier = "==0.30.0" },
{ name = "attrs", specifier = "==25.3.0" },
{ name = "bcrypt", specifier = "==4.3.0" },
{ name = "certifi", specifier = "==2025.8.3" },
{ name = "charset-normalizer", specifier = "==3.4.2" },
{ name = "click", specifier = "==8.2.1" },
{ name = "cloudscraper", specifier = "==1.2.71" },
{ name = "fastapi", specifier = "==0.116.1" },
{ name = "gppt", specifier = "==4.1.1" },
{ name = "h11", specifier = "==0.16.0" },
{ name = "idna", specifier = "==3.10" },
{ name = "loguru", specifier = "==0.7.3" },
{ name = "outcome", specifier = "==1.3.0.post0" },
{ name = "passlib", specifier = "==1.7.4" },
{ name = "pixivpy3", specifier = "==3.7.5" },
{ name = "pwinput", specifier = "==1.0.3" },
{ name = "pydantic", specifier = "==2.11.7" },
{ name = "pydantic-core", specifier = "==2.33.2" },
{ name = "pyjwt", specifier = "==2.10.1" },
{ name = "pyparsing", specifier = "==3.2.3" },
{ name = "pysocks", specifier = "==1.7.1" },
{ name = "python-decouple", specifier = "==3.8" },
{ name = "python-multipart", specifier = "==0.0.20" },
{ name = "requests", specifier = "==2.32.4" },
{ name = "requests-toolbelt", specifier = "==1.0.0" },
{ name = "selenium", specifier = "==4.34.2" },
{ name = "sniffio", specifier = "==1.3.1" },
{ name = "sortedcontainers", specifier = "==2.4.0" },
{ name = "starlette", specifier = "==0.47.2" },
{ name = "trio", specifier = "==0.30.0" },
{ name = "trio-websocket", specifier = "==0.12.2" },
{ name = "typing-extensions", specifier = "==4.14.1" },
{ name = "typing-inspection", specifier = "==0.4.1" },
{ name = "urllib3", specifier = "==2.5.0" },
{ name = "uvicorn", specifier = "==0.35.0" },
{ name = "websocket-client", specifier = "==1.8.0" },
{ name = "wsproto", specifier = "==1.2.0" },
]
[[package]]
name = "pixivpy3"
version = "3.7.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cloudscraper" },
{ name = "requests" },
{ name = "requests-toolbelt" },
{ name = "typing-extensions" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/24/19/4a6798a8552dc5702618e3e340b995de959849704458392f4adbfcd697c8/pixivpy3-3.7.5.tar.gz", hash = "sha256:cfb0c444bea994a9b2c1c01e60f2e8ab1afe57b05f2d51545ef64a7df2f220cc", size = 20527, upload-time = "2024-03-03T10:09:50.747Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/09/f0/8f40168c6e4aa824543d67d17b3104f00c6e9bda53709693c1a0e30e0b72/pixivpy3-3.7.5-py3-none-any.whl", hash = "sha256:3f9c3c9236d9924de9f80390cc3d1cc78bc494175a94cc9903f7b60308efca72", size = 17549, upload-time = "2024-03-03T10:09:48.48Z" },
]
[[package]]
name = "pwinput"
version = "1.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4b/c9/f6d03c2214c282251baa38c6be08f5b9f81f1ea488716cd2335e7e190b58/pwinput-1.0.3.tar.gz", hash = "sha256:ca1a8bd06e28872d751dbd4132d8637127c25b408ea3a349377314a5491426f3", size = 4433, upload-time = "2023-02-16T17:04:14.592Z" }
[[package]]
name = "pycparser"
version = "3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
]
[[package]]
name = "pydantic"
version = "2.11.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" },
]
[[package]]
name = "pydantic-core"
version = "2.33.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" },
{ url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" },
{ url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" },
{ url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" },
{ url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" },
{ url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" },
{ url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" },
{ url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" },
{ url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" },
{ url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" },
{ url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" },
{ url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" },
{ url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" },
{ url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" },
{ url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" },
{ url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" },
{ url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" },
]
[[package]]
name = "pyjwt"
version = "2.10.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" },
]
[[package]]
name = "pyparsing"
version = "3.2.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" },
]
[[package]]
name = "pysocks"
version = "1.7.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" },
]
[[package]]
name = "python-decouple"
version = "3.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e1/97/373dcd5844ec0ea5893e13c39a2c67e7537987ad8de3842fe078db4582fa/python-decouple-3.8.tar.gz", hash = "sha256:ba6e2657d4f376ecc46f77a3a615e058d93ba5e465c01bbe57289bfb7cce680f", size = 9612, upload-time = "2023-03-01T19:38:38.143Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/d4/9193206c4563ec771faf2ccf54815ca7918529fe81f6adb22ee6d0e06622/python_decouple-3.8-py3-none-any.whl", hash = "sha256:d0d45340815b25f4de59c974b855bb38d03151d81b037d9e3f463b0c9f8cbd66", size = 9947, upload-time = "2023-03-01T19:38:36.015Z" },
]
[[package]]
name = "python-multipart"
version = "0.0.20"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" },
]
[[package]]
name = "requests"
version = "2.32.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" },
]
[[package]]
name = "requests-toolbelt"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" },
]
[[package]]
name = "selenium"
version = "4.34.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "trio" },
{ name = "trio-websocket" },
{ name = "typing-extensions" },
{ name = "urllib3", extra = ["socks"] },
{ name = "websocket-client" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b0/e6/646d0a41fb9a64572043c3de80be2a4941f2aeb578f273cf3dae54fc9437/selenium-4.34.2.tar.gz", hash = "sha256:0f6d147595f08c6d4bad87b34c39dcacb4650aedc78e3956c8eac1bb752a3854", size = 896309, upload-time = "2025-07-08T12:54:54.785Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/2b/dee1c58bde0a747b2d75fa7282a190885a726fe95b18b8ce1dc52f9c0983/selenium-4.34.2-py3-none-any.whl", hash = "sha256:ea208f7db9e3b26e58c4a817ea9dd29454576d6ea55937d754df079ad588e1ad", size = 9410676, upload-time = "2025-07-08T12:54:48.725Z" },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]]
name = "sortedcontainers"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
]
[[package]]
name = "starlette"
version = "0.47.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", size = 2583948, upload-time = "2025-07-20T17:31:58.522Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload-time = "2025-07-20T17:31:56.738Z" },
]
[[package]]
name = "trio"
version = "0.30.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "attrs" },
{ name = "cffi", marker = "implementation_name != 'pypy' and os_name == 'nt'" },
{ name = "idna" },
{ name = "outcome" },
{ name = "sniffio" },
{ name = "sortedcontainers" },
]
sdist = { url = "https://files.pythonhosted.org/packages/01/c1/68d582b4d3a1c1f8118e18042464bb12a7c1b75d64d75111b297687041e3/trio-0.30.0.tar.gz", hash = "sha256:0781c857c0c81f8f51e0089929a26b5bb63d57f927728a5586f7e36171f064df", size = 593776, upload-time = "2025-04-21T00:48:19.507Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/69/8e/3f6dfda475ecd940e786defe6df6c500734e686c9cd0a0f8ef6821e9b2f2/trio-0.30.0-py3-none-any.whl", hash = "sha256:3bf4f06b8decf8d3cf00af85f40a89824669e2d033bb32469d34840edcfc22a5", size = 499194, upload-time = "2025-04-21T00:48:17.167Z" },
]
[[package]]
name = "trio-websocket"
version = "0.12.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "outcome" },
{ name = "trio" },
{ name = "wsproto" },
]
sdist = { url = "https://files.pythonhosted.org/packages/d1/3c/8b4358e81f2f2cfe71b66a267f023a91db20a817b9425dd964873796980a/trio_websocket-0.12.2.tar.gz", hash = "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae", size = 33549, upload-time = "2025-02-25T05:16:58.947Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/19/eb640a397bba49ba49ef9dbe2e7e5c04202ba045b6ce2ec36e9cadc51e04/trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6", size = 21221, upload-time = "2025-02-25T05:16:57.545Z" },
]
[[package]]
name = "typing-extensions"
version = "4.14.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" },
]
[[package]]
name = "typing-inspection"
version = "0.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" },
]
[[package]]
name = "urllib3"
version = "2.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
]
[package.optional-dependencies]
socks = [
{ name = "pysocks" },
]
[[package]]
name = "uvicorn"
version = "0.35.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" },
]
[[package]]
name = "websocket-client"
version = "1.8.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" },
]
[[package]]
name = "win32-setctime"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" },
]
[[package]]
name = "wsproto"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c9/4a/44d3c295350d776427904d73c189e10aeae66d7f555bb2feee16d1e4ba5a/wsproto-1.2.0.tar.gz", hash = "sha256:ad565f26ecb92588a3e43bc3d96164de84cd9902482b130d0ddbaa9664a85065", size = 53425, upload-time = "2022-08-23T19:58:21.447Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/58/e860788190eba3bcce367f74d29c4675466ce8dddfba85f7827588416f01/wsproto-1.2.0-py3-none-any.whl", hash = "sha256:b9acddd652b585d75b20477888c56642fdade28bdfd3579aa24a4d2c037dd736", size = 24226, upload-time = "2022-08-23T19:58:19.96Z" },
]