reaplce shared psycopg2 connection with psycopg3 async pool

This commit is contained in:
leander committed 2026-10-24 05:16:24 +02:00
1 parent b5ed18ee42
commit 01bccceb6d
12 files changed
+390 -228

No files matched your search

+2 -1
View File
@@ -17,7 +17,8 @@ dependencies = [
"opencv-python==5.0.*",
"pillow>=9.2,<12.0",
"psutil==7.2.*",
"psycopg2-binary==2.9.*",
"psycopg-pool==3.3.*",
"psycopg[binary]==3.3.*",
"pypandoc==1.17.*",
"pyyaml==6.0.*",
"regex==2026.9.29",
+23 -24
View File
@@ -25,17 +25,21 @@ class Birthday(commands.Cog):
"""
self.bot = bot
self.cursor = self.bot.db_connection.cursor()
self.init_db()
self.cursor.execute("SET timezone TO 'Europe/Oslo';")
async def cog_load(self):
"""
Set up the database before the cog is used
"""
await self.init_db()
self.birthday_check.start()
def init_db(self):
async def init_db(self):
"""
Create the necessary tables for the birthday cog to work
"""
self.cursor.execute("""
await self.bot.db.execute("""
CREATE TABLE IF NOT EXISTS birthdays (
discord_id BIGINT PRIMARY KEY,
birthday DATE
@@ -57,7 +61,7 @@ class Birthday(commands.Cog):
self.bot.logger.info("Checking for birthdays")
self.cursor.execute("""
birthdays = await self.bot.db.fetchall("""
SELECT *
FROM birthdays
WHERE (
@@ -71,7 +75,6 @@ class Birthday(commands.Cog):
);
""")
birthdays = self.cursor.fetchall()
self.bot.logger.info(f"Found the following birthdays: {birthdays}")
if not birthdays:
@@ -140,9 +143,8 @@ class Birthday(commands.Cog):
async def cog_unload(self):
self.bot.logger.info("Unloading cog")
self.birthday_check.cancel()
self.cursor.close()
def fetch_user_birthday(self, user_id: int) -> datetime | None:
async def fetch_user_birthday(self, user_id: int) -> datetime | None:
"""
Fetch the birthday of a user in the database
@@ -155,13 +157,12 @@ class Birthday(commands.Cog):
(datetime | None): The birthday of the user
"""
self.cursor.execute("SELECT birthday FROM birthdays WHERE discord_id = (%s)", (user_id,))
result = self.cursor.fetchone()
result = await self.bot.db.fetchone("SELECT birthday FROM birthdays WHERE discord_id = (%s)", (user_id,))
if result:
birthday = result[0]
return datetime(birthday.year, birthday.month, birthday.day)
def fetch_user_next_birthday(self, user_id: int) -> datetime:
async def fetch_user_next_birthday(self, user_id: int) -> datetime:
"""
Get date of next birthday for a user
@@ -174,7 +175,7 @@ class Birthday(commands.Cog):
(datetime): The date of the next birthday
"""
self.cursor.execute(
results = await self.bot.db.fetchone(
"""
SELECT *, CAST(birthday + ((EXTRACT(YEAR FROM AGE(birthday)) + 1) * interval '1' YEAR) AS DATE) AS next_bday
FROM birthdays
@@ -182,7 +183,6 @@ class Birthday(commands.Cog):
""",
(user_id,),
)
results = self.cursor.fetchone()
if results:
birthday = results[2]
return datetime(birthday.year, birthday.month, birthday.day)
@@ -190,7 +190,7 @@ class Birthday(commands.Cog):
# This will never happen unless a database error occurs. Keeping the type checker happy
return datetime.now()
def fetch_next_birthdays(self) -> list[tuple[int, datetime, datetime]]:
async def fetch_next_birthdays(self) -> list[tuple[int, datetime, datetime]]:
"""
Fetch the 5 first future birthdays of all users in the database
@@ -199,12 +199,11 @@ class Birthday(commands.Cog):
(list[tuple[int, datetime, datetime]]): List of tuples containing the user ID, date of birth and next birthday
"""
self.cursor.execute("""
results = await self.bot.db.fetchall("""
SELECT *, CAST(birthday + ((EXTRACT(YEAR FROM AGE(birthday)) + 1) * interval '1' YEAR) AS DATE) AS next_bday
FROM birthdays
ORDER BY next_bday ASC
""")
results = self.cursor.fetchall()
# Convert date objects to datetime objects
birthdays = []
@@ -216,7 +215,7 @@ class Birthday(commands.Cog):
return birthdays
def set_user_birthday(self, user_id: int, birthday: datetime):
async def set_user_birthday(self, user_id: int, birthday: datetime):
"""
Set the birthday of a user in the database. If it already exists, update it.
@@ -226,7 +225,7 @@ class Birthday(commands.Cog):
birthday (datetime): The birthday of the user
"""
self.cursor.execute(
await self.bot.db.execute(
"""
INSERT INTO birthdays (discord_id, birthday)
VALUES (%s, %s)
@@ -275,7 +274,7 @@ class Birthday(commands.Cog):
ephemeral=True,
)
self.set_user_birthday(interaction.user.id, date)
await self.set_user_birthday(interaction.user.id, date)
embed = embed_templates.success(f"Bursdag satt til {discord.utils.format_dt(date, style='D')}")
await interaction.response.send_message(embed=embed)
@@ -292,7 +291,7 @@ class Birthday(commands.Cog):
interaction (discord.Interaction): Slash command context object
"""
self.cursor.execute("DELETE FROM birthdays WHERE discord_id = (%s)", (interaction.user.id,))
await self.bot.db.execute("DELETE FROM birthdays WHERE discord_id = (%s)", (interaction.user.id,))
embed = embed_templates.success("Bursdag fjernet")
await interaction.response.send_message(embed=embed)
@@ -315,7 +314,7 @@ class Birthday(commands.Cog):
if not bruker:
bruker = interaction.user
user = self.fetch_user_birthday(bruker.id)
user = await self.fetch_user_birthday(bruker.id)
if not user:
return await interaction.response.send_message(
@@ -328,7 +327,7 @@ class Birthday(commands.Cog):
years_old = relativedelta(datetime.now(), birthday).years
# Next birthday information
next_birthday = self.fetch_user_next_birthday(bruker.id)
next_birthday = await self.fetch_user_next_birthday(bruker.id)
next_birthday_days = (next_birthday.date() - datetime.now().date()).days
# Japanese era
@@ -366,7 +365,7 @@ class Birthday(commands.Cog):
await interaction.response.defer()
next_birthdays = self.fetch_next_birthdays()
next_birthdays = await self.fetch_next_birthdays()
if not next_birthdays:
return await interaction.followup.send(embed=embed_templates.error_warning("Ingen bursdager"))
+17 -16
View File
@@ -16,28 +16,33 @@ class Gullkorn(commands.Cog):
"""
self.bot = bot
self.cursor = self.bot.db_connection.cursor()
self.init_db()
def init_db(self):
async def cog_load(self):
"""
Set up the database before the cog is used
"""
await self.init_db()
async def init_db(self):
"""
Create the necessary tables for the gullkorn cog to work
"""
self.cursor.execute("""
await self.bot.db.execute("""
CREATE TABLE IF NOT EXISTS gullkorn (
discord_id BIGINT PRIMARY KEY,
times_cited INT NOT NULL DEFAULT 0,
citations_posted INT NOT NULL DEFAULT 0
);
""")
self.cursor.execute("""
await self.bot.db.execute("""
CREATE OR REPLACE VIEW most_cited AS
SELECT discord_id, times_cited
FROM gullkorn
ORDER BY times_cited DESC;
""")
self.cursor.execute("""
await self.bot.db.execute("""
CREATE OR REPLACE VIEW most_frequent_posters AS
SELECT discord_id, citations_posted
FROM gullkorn
@@ -81,7 +86,7 @@ class Gullkorn(commands.Cog):
return
for user in message.mentions:
self.cursor.execute(
await self.bot.db.execute(
"""
INSERT INTO gullkorn (discord_id, times_cited, citations_posted)
VALUES (%s, 1, 0)
@@ -91,7 +96,7 @@ class Gullkorn(commands.Cog):
(user.id,),
)
self.cursor.execute(
await self.bot.db.execute(
"""
INSERT INTO gullkorn (discord_id, times_cited, citations_posted)
VALUES (%s, 0, 1)
@@ -120,7 +125,7 @@ class Gullkorn(commands.Cog):
# This looks kinda ugly, ngl
if bruker:
self.cursor.execute(
result = await self.bot.db.fetchone(
"""
SELECT *
FROM gullkorn
@@ -128,7 +133,6 @@ class Gullkorn(commands.Cog):
""",
(bruker.id,),
)
result = self.cursor.fetchone()
if not result:
return await interaction.response.send_message(
@@ -145,25 +149,22 @@ class Gullkorn(commands.Cog):
embed.add_field(name="Antall gullkorn postet", value=result[2])
return await interaction.response.send_message(embed=embed, ephemeral=False)
self.cursor.execute("""
summary = await self.bot.db.fetchone("""
SELECT SUM(citations_posted)
FROM gullkorn;
""")
summary = self.cursor.fetchone()
self.cursor.execute("""
most_cited = await self.bot.db.fetchall("""
SELECT *
FROM most_cited
LIMIT 5;
""")
most_cited = self.cursor.fetchall()
self.cursor.execute("""
citations_posted = await self.bot.db.fetchall("""
SELECT *
FROM most_frequent_posters
LIMIT 5;
""")
citations_posted = self.cursor.fetchall()
most_cited_string = self.construct_data_string(most_cited)
citations_posted_string = self.construct_data_string(citations_posted)
+15 -11
View File
@@ -17,15 +17,20 @@ class MCWhitelist(commands.Cog):
"""
self.bot = bot
self.cursor = self.bot.db_connection.cursor()
self.init_db()
def init_db(self):
async def cog_load(self):
"""
Set up the database before the cog is used
"""
await self.init_db()
async def init_db(self):
"""
Create the necessary tables for the mc_whitelist cog to work
"""
self.cursor.execute("""
await self.bot.db.execute("""
CREATE TABLE IF NOT EXISTS mc_whitelist (
discord_id BIGINT PRIMARY KEY,
minecraft_id TEXT NOT NULL
@@ -61,15 +66,14 @@ class MCWhitelist(commands.Cog):
data = await response.json(content_type=None)
# Check if the discord user or minecraft user is in the db
self.cursor.execute(
if await self.bot.db.fetchone(
"""
SELECT *
FROM mc_whitelist
WHERE minecraft_id = %s OR discord_id = %s
""",
(data["id"], interaction.user.id),
)
if self.cursor.fetchone():
):
return await interaction.response.send_message(
embed=embed_templates.error_warning(
"Du har allerede whitelisted en bruker eller så er brukeren du oppga whitelisted"
@@ -92,7 +96,7 @@ class MCWhitelist(commands.Cog):
)
# Add user to db
self.cursor.execute(
await self.bot.db.execute(
"""
INSERT INTO mc_whitelist (discord_id, minecraft_id)
VALUES (%s, %s)
@@ -119,7 +123,7 @@ class MCWhitelist(commands.Cog):
"""
# Check if the discord user is in the db
self.cursor.execute(
result = await self.bot.db.fetchone(
"""
SELECT *
FROM mc_whitelist
@@ -127,7 +131,7 @@ class MCWhitelist(commands.Cog):
""",
(interaction.user.id,),
)
if not (result := self.cursor.fetchone()):
if not result:
return await interaction.response.send_message(
embed=embed_templates.error_warning("Du har ikke en whitelisted bruker på vår minecraftserver")
)
@@ -149,7 +153,7 @@ class MCWhitelist(commands.Cog):
)
# Remove user from db
self.cursor.execute(
await self.bot.db.execute(
"""
DELETE FROM mc_whitelist
WHERE discord_id = %s
+16 -14
View File
@@ -42,15 +42,20 @@ class SocialCredit(commands.Cog):
self.START_POINTS = 1000
self.STAR_THRESHOLD = 3
self.POINTS_PER_STAR = 25
self.cursor = self.bot.db_connection.cursor()
self.init_db()
def init_db(self):
async def cog_load(self):
"""
Set up the database before the cog is used
"""
await self.init_db()
async def init_db(self):
"""
Create the necessary tables for the social credit cog to work
"""
self.cursor.execute("""
await self.bot.db.execute("""
CREATE TABLE IF NOT EXISTS social_credit (
user_id BIGINT PRIMARY KEY,
credit_score SMALLINT NOT NULL
@@ -65,7 +70,7 @@ class SocialCredit(commands.Cog):
async def wrapper(*args, **kwargs):
self = args[0]
self.add_citizen(args[1])
await self.add_citizen(args[1])
await func(*args, **kwargs)
return wrapper
@@ -84,7 +89,7 @@ class SocialCredit(commands.Cog):
return decorator
def add_citizen(self, user_id: int):
async def add_citizen(self, user_id: int):
"""
Adds a user to the database if they aren't already in it
@@ -93,7 +98,7 @@ class SocialCredit(commands.Cog):
user_id (int): The user's Discord ID
"""
self.cursor.execute(
await self.bot.db.execute(
"""
INSERT INTO social_credit
(user_id, credit_score)
@@ -142,7 +147,6 @@ class SocialCredit(commands.Cog):
async def cog_unload(self):
self.bot.logger.info("Unloading cog")
self.fuck_uwu.cancel()
self.cursor.close()
@add_new_citizen
async def social_punishment(self, user_id: int, points: int, reason: str):
@@ -157,7 +161,7 @@ class SocialCredit(commands.Cog):
"""
self.bot.logger.info(f"{points} points deducted from {user_id} ({reason})")
self.cursor.execute(
await self.bot.db.execute(
"""
UPDATE social_credit
SET credit_score = credit_score - %s
@@ -179,7 +183,7 @@ class SocialCredit(commands.Cog):
"""
self.bot.logger.info(f"{points} points given to {user_id} ({reason})")
self.cursor.execute(
await self.bot.db.execute(
"""
UPDATE social_credit
SET credit_score = credit_score + %s
@@ -206,14 +210,13 @@ class SocialCredit(commands.Cog):
if not bruker:
bruker = interaction.user
self.cursor.execute(
result = await self.bot.db.fetchone(
"""
SELECT * FROM social_credit
WHERE user_id = %s
""",
(bruker.id,),
)
result = self.cursor.fetchone()
if not result:
return await interaction.response.send_message(
@@ -239,11 +242,10 @@ class SocialCredit(commands.Cog):
await interaction.response.defer()
self.cursor.execute("""
result = await self.bot.db.fetchall("""
SELECT * FROM social_credit
ORDER BY credit_score DESC
""")
result = self.cursor.fetchall()
if not result:
return await interaction.followup.send(
+19 -18
View File
@@ -1,7 +1,7 @@
import re
import discord
import psycopg2
import psycopg
from discord.ext import commands
from discord.ext import tasks
@@ -25,16 +25,20 @@ class StarboardScraper(commands.Cog):
self.bot = bot
self.starboard_channel_id = self.bot.channel_ids.get("starboard")
self.cursor = self.bot.db_connection.cursor()
self.init_db()
async def cog_load(self):
"""
Set up the database before the cog is used
"""
await self.init_db()
self.star_check.start()
def init_db(self):
async def init_db(self):
"""
Create the necessary tables for the cog to work
"""
self.cursor.execute("""
await self.bot.db.execute("""
CREATE TABLE IF NOT EXISTS starboard_messages (
id SERIAL PRIMARY KEY,
starboard_message_id BIGINT UNIQUE,
@@ -42,7 +46,7 @@ class StarboardScraper(commands.Cog):
original_channel_id BIGINT
)
""")
self.cursor.execute("""
await self.bot.db.execute("""
CREATE TABLE IF NOT EXISTS starboard_scraper_state (
id INTEGER PRIMARY KEY DEFAULT 1,
last_scraped_message_id BIGINT,
@@ -51,7 +55,7 @@ class StarboardScraper(commands.Cog):
)
""")
def get_last_scraped_message_id(self):
async def get_last_scraped_message_id(self):
"""
Get the ID of the last scraped message
@@ -59,15 +63,14 @@ class StarboardScraper(commands.Cog):
-------
int or None: Last scraped message ID or None if never scraped
"""
self.cursor.execute("""
result = await self.bot.db.fetchone("""
SELECT last_scraped_message_id
FROM starboard_scraper_state
WHERE id = 1
""")
result = self.cursor.fetchone()
return result[0] if result else None
def update_last_scraped_message_id(self, message_id: int):
async def update_last_scraped_message_id(self, message_id: int):
"""
Update the last scraped message ID
@@ -75,7 +78,7 @@ class StarboardScraper(commands.Cog):
----------
message_id (int): The message ID to store
"""
self.cursor.execute(
await self.bot.db.execute(
"""
INSERT INTO starboard_scraper_state (id, last_scraped_message_id, last_scrape_time)
VALUES (1, %s, CURRENT_TIMESTAMP)
@@ -85,7 +88,6 @@ class StarboardScraper(commands.Cog):
""",
(message_id,),
)
self.bot.db_connection.commit()
def parse_starboard_message(self, message: discord.Message):
"""
@@ -130,7 +132,7 @@ class StarboardScraper(commands.Cog):
self.bot.logger.error(f"Could not find starboard channel {self.starboard_channel_id}")
return
last_scraped_id = self.get_last_scraped_message_id()
last_scraped_id = await self.get_last_scraped_message_id()
if last_scraped_id:
self.bot.logger.info(f"Resuming scrape from msg ID {last_scraped_id}")
@@ -159,7 +161,7 @@ class StarboardScraper(commands.Cog):
original_message_id, original_channel_id = parsed_ids
try:
self.cursor.execute(
inserted = await self.bot.db.execute(
"""
INSERT INTO starboard_messages
(starboard_message_id, original_message_id, original_channel_id)
@@ -169,16 +171,16 @@ class StarboardScraper(commands.Cog):
(message.id, original_message_id, original_channel_id),
)
if self.cursor.rowcount > 0:
if inserted > 0:
new_count += 1
except psycopg2.Error as e:
except psycopg.Error as e:
self.bot.logger.error(f"Database error for message {message.id}: {e}")
error_count += 1
continue
if latest_message_id:
self.update_last_scraped_message_id(latest_message_id)
await self.update_last_scraped_message_id(latest_message_id)
self.bot.logger.info(f"Starboard scrape complete: {new_count} new messages, {error_count} errors")
@@ -201,7 +203,6 @@ class StarboardScraper(commands.Cog):
async def cog_unload(self):
self.bot.logger.info("Unloading cog")
self.star_check.cancel()
self.cursor.close()
async def setup(bot: commands.Bot):
+37 -33
View File
@@ -23,22 +23,25 @@ class Streak(commands.Cog):
self.bot = bot
self.cursor = self.bot.db_connection.cursor()
self.init_db()
# Cache to avoid having to insert to the db for every message
self.streak_cache = {}
self.populate_cache()
async def cog_load(self):
"""
Set up the database before the cog is used
"""
await self.init_db()
await self.populate_cache()
self.streak_check.start()
self.insert_cache_loop.start()
def init_db(self):
async def init_db(self):
"""
Create the necessary tables for the streak cog to work
"""
self.cursor.execute("""
await self.bot.db.execute("""
CREATE TABLE IF NOT EXISTS streak (
discord_id BIGINT PRIMARY KEY,
streak_start_id TEXT NOT NULL,
@@ -52,18 +55,16 @@ class Streak(commands.Cog):
self.insert_cache_loop.cancel()
self.streak_check.cancel()
await self.insert_cache()
self.cursor.close()
def populate_cache(self):
async def populate_cache(self):
"""
Populate the cache with the current streaks
"""
self.cursor.execute("""
streaks = await self.bot.db.fetchall("""
SELECT *
FROM streak;
""")
streaks = self.cursor.fetchall()
for streak in streaks:
self.streak_cache[streak[0]] = {
@@ -111,23 +112,29 @@ class Streak(commands.Cog):
self.bot.logger.info("Inserting cache into database")
# Insert into database
for user_id, user_data in self.streak_cache.items():
self.cursor.execute(
"""
INSERT INTO streak (discord_id, streak_start_id, streak_start_time, latest_post_time)
VALUES (%s, %s, %s, %s)
ON CONFLICT (discord_id) DO UPDATE
SET latest_post_time = %s;
""",
(
user_id,
user_data["first_post_id"],
user_data["first_post_time"],
user_data["latest_post_time"],
user_data["latest_post_time"],
),
# Snapshot the cache before awaiting, since new messages can change it while we wait for the database
rows = [
(
user_id,
user_data["first_post_id"],
user_data["first_post_time"],
user_data["latest_post_time"],
user_data["latest_post_time"],
)
for user_id, user_data in self.streak_cache.items()
]
if not rows:
return
await self.bot.db.executemany(
"""
INSERT INTO streak (discord_id, streak_start_id, streak_start_time, latest_post_time)
VALUES (%s, %s, %s, %s)
ON CONFLICT (discord_id) DO UPDATE
SET latest_post_time = %s;
""",
rows,
)
@tasks.loop(time=misc_utils.MIDNIGHT)
@misc_utils.log_errors
@@ -142,17 +149,16 @@ class Streak(commands.Cog):
if self.streak_cache:
await self.insert_cache()
self.cursor.execute("""
streaks = await self.bot.db.fetchall("""
SELECT discord_id, latest_post_time
FROM streak;
""")
streaks = self.cursor.fetchall()
for streak in streaks:
if (datetime.now() - streak[1]).days >= 1:
self.bot.logger.info(f"User {streak[0]} lost their streak")
self.streak_cache.pop(streak[0])
self.cursor.execute(
await self.bot.db.execute(
"""
DELETE FROM streak
WHERE discord_id = %s;
@@ -180,7 +186,7 @@ class Streak(commands.Cog):
if not bruker:
bruker = interaction.user
self.cursor.execute(
streak = await self.bot.db.fetchone(
"""
SELECT streak_start_id, streak_start_time::timestamptz
FROM streak
@@ -188,7 +194,6 @@ class Streak(commands.Cog):
""",
(bruker.id,),
)
streak = self.cursor.fetchone()
if not streak:
return await interaction.followup.send(embed=embed_templates.error_warning("Brukeren har ikke noen streak"))
@@ -231,12 +236,11 @@ class Streak(commands.Cog):
await interaction.response.defer()
self.cursor.execute("""
streaks = await self.bot.db.fetchall("""
SELECT discord_id, streak_start_time
FROM streak
ORDER BY streak_start_time;
""")
streaks = self.cursor.fetchall()
if not streaks:
return await interaction.followup.send(embed=embed_templates.error_warning("Ingen har noen streak enda"))
+21 -18
View File
@@ -14,8 +14,6 @@ from cogs.utils import misc_utils
class UserFacts(commands.Cog):
def __init__(self, bot):
self.bot = bot
self.cursor = self.bot.db_connection.cursor()
self.init_db()
self.mbti_codes = {
"INTJ",
@@ -38,12 +36,19 @@ class UserFacts(commands.Cog):
self.mbti_list = list(self.mbti_codes)
self.similarity_matrix = self.create_similarity_matrix()
def init_db(self):
async def cog_load(self):
"""
Set up the database before the cog is used
"""
await self.init_db()
async def init_db(self):
"""
Create the necessary tables for the birthday cog to work
"""
self.cursor.execute("""
await self.bot.db.execute("""
CREATE TABLE IF NOT EXISTS user_facts (
discord_id BIGINT PRIMARY KEY,
mbti CHAR(4),
@@ -69,14 +74,13 @@ class UserFacts(commands.Cog):
if not bruker:
bruker = interaction.user
self.cursor.execute(
height = await self.bot.db.fetchone(
"""
SELECT height FROM user_facts
WHERE discord_id = %s and height IS NOT NULL;
""",
(bruker.id,),
)
height = self.cursor.fetchone()
if not height:
return await interaction.response.send_message(
@@ -110,7 +114,7 @@ class UserFacts(commands.Cog):
height_cm (int): The height of the user
"""
self.cursor.execute(
await self.bot.db.execute(
"""
INSERT INTO user_facts (discord_id, height)
VALUES (%s, %s)
@@ -134,7 +138,7 @@ class UserFacts(commands.Cog):
interaction (discord.Interaction): The interaction object
"""
self.cursor.execute(
updated = await self.bot.db.execute(
"""
UPDATE user_facts
SET height = NULL
@@ -143,7 +147,7 @@ class UserFacts(commands.Cog):
(interaction.user.id,),
)
if self.cursor.rowcount == 0:
if updated == 0:
return await interaction.response.send_message(
embed=embed_templates.success("Du hadde ikke lagt inn høyden din fra før av men ok :)")
)
@@ -162,13 +166,13 @@ class UserFacts(commands.Cog):
interaction (discord.Interaction): The interaction object
"""
self.cursor.execute("""
result = await self.bot.db.fetchall("""
SELECT discord_id, height FROM user_facts
WHERE height IS NOT NULL
ORDER BY height DESC;
""")
if not (result := self.cursor.fetchall()):
if not result:
return await interaction.response.send_message(
embed=embed_templates.error_warning("Ingen har lagt inn høyden sin enda")
)
@@ -201,14 +205,13 @@ class UserFacts(commands.Cog):
await interaction.response.defer()
self.cursor.execute(
user_mbti = await self.bot.db.fetchone(
"""
SELECT mbti FROM user_facts
WHERE discord_id = %s AND mbti IS NOT NULL;
""",
(bruker.id,),
)
user_mbti = self.cursor.fetchone()
if not user_mbti:
return await interaction.followup.send(
@@ -220,7 +223,7 @@ class UserFacts(commands.Cog):
embed = discord.Embed(color=bruker.color, title="MBTI", description=user_mbti)
embed.set_author(name=bruker.global_name, icon_url=bruker.avatar)
self.cursor.execute(
results = await self.bot.db.fetchall(
"""
SELECT discord_id, mbti FROM user_facts
WHERE mbti IS NOT NULL and discord_id != %s;
@@ -228,7 +231,7 @@ class UserFacts(commands.Cog):
(bruker.id,),
)
if not (results := self.cursor.fetchall()):
if not results:
return await interaction.followup.send(embed=embed)
others = []
@@ -264,7 +267,7 @@ class UserFacts(commands.Cog):
if mbti.upper() not in self.mbti_codes:
return await interaction.response.send_message(embed=embed_templates.error_warning("Ugyldig MBTI"))
self.cursor.execute(
await self.bot.db.execute(
"""
INSERT INTO user_facts (discord_id, mbti)
VALUES (%s, %s)
@@ -303,7 +306,7 @@ class UserFacts(commands.Cog):
interaction (discord.Interaction): The interaction object
"""
self.cursor.execute(
updated = await self.bot.db.execute(
"""
UPDATE user_facts
SET mbti = NULL
@@ -312,7 +315,7 @@ class UserFacts(commands.Cog):
(interaction.user.id,),
)
if self.cursor.rowcount == 0:
if updated == 0:
return await interaction.response.send_message(
embed=embed_templates.success("Du hadde ikke lagt inn MBTIen din fra før av men ok :)")
)
+97
View File
@@ -0,0 +1,97 @@
from typing import Any
from typing import Iterable
from psycopg_pool import AsyncConnectionPool
class Database:
"""Runs queries on connections borrowed from a connection pool"""
def __init__(self, pool: AsyncConnectionPool):
"""
Parameters
----------
pool (AsyncConnectionPool): The pool to borrow connections from
"""
self.pool = pool
async def execute(self, query: str, params: Iterable[Any] | None = None) -> int:
"""
Runs a single statement
Parameters
----------
query (str): The SQL statement
params (Iterable[Any] | None): Values for the statement's placeholders
Returns
----------
(int): The number of rows affected
"""
async with self.pool.connection() as conn:
cursor = await conn.execute(query, params)
return cursor.rowcount
async def execute_in_transaction(self, statements: list[tuple[str, Iterable[Any]]]):
"""
Runs several statements in one transaction, so either all or none of them take effect
Parameters
----------
statements (list[tuple[str, Iterable[Any]]]): The SQL statements and their placeholder values
"""
async with self.pool.connection() as conn, conn.transaction():
for query, params in statements:
await conn.execute(query, params)
async def executemany(self, query: str, params_seq: Iterable[Iterable[Any]]):
"""
Runs a statement once for each set of values
Parameters
----------
query (str): The SQL statement
params_seq (Iterable[Iterable[Any]]): The values for each run
"""
async with self.pool.connection() as conn, conn.cursor() as cursor:
await cursor.executemany(query, params_seq)
async def fetchone(self, query: str, params: Iterable[Any] | None = None) -> tuple | None:
"""
Runs a query and returns its first row
Parameters
----------
query (str): The SQL query
params (Iterable[Any] | None): Values for the query's placeholders
Returns
----------
(tuple | None): The first row. None if there are no rows
"""
async with self.pool.connection() as conn:
cursor = await conn.execute(query, params)
return await cursor.fetchone()
async def fetchall(self, query: str, params: Iterable[Any] | None = None) -> list[tuple]:
"""
Runs a query and returns all its rows
Parameters
----------
query (str): The SQL query
params (Iterable[Any] | None): Values for the query's placeholders
Returns
----------
(list[tuple]): The rows
"""
async with self.pool.connection() as conn:
cursor = await conn.execute(query, params)
return await cursor.fetchall()
+34 -37
View File
@@ -9,13 +9,12 @@ from io import StringIO
import discord
import nltk
import numpy as np
import psycopg2
import psycopg
from discord import app_commands
from discord.ext import commands
from discord.ext import tasks
from nltk.corpus import stopwords
from PIL import Image
from psycopg2.extras import execute_batch
from wordcloud import WordCloud as WCloud # Avoid naming conflicts with cog class name
from cogs.utils import embed_templates
@@ -33,8 +32,6 @@ class WordCloud(commands.Cog):
"""
self.bot = bot
self.cursor = self.bot.db_connection.cursor()
self.init_db()
# Default dict where all users are empty defaultdicts
# The user's defaultdict has a default value of 0
@@ -43,30 +40,36 @@ class WordCloud(commands.Cog):
# We cache the consenting users to avoid querying the database every 16:36
# It's frail but it works unless you have a skill issue
self.consenting_users = []
self.populate_consenting_users()
try:
nltk.data.find("corpora/stopwords")
except LookupError:
nltk.download("stopwords")
self.insert_cache_loop.start()
self.MSG_NO_DATA = "Fant ingen data om deg" # SonarCloud recommended this LMAO. Makes sense but also not
def init_db(self):
async def cog_load(self):
"""
Set up the database before the cog is used
"""
await self.init_db()
await self.populate_consenting_users()
self.insert_cache_loop.start()
async def init_db(self):
"""
Create the necessary tables for the Ordsky cog to work
"""
self.cursor.execute("""
await self.bot.db.execute("""
CREATE TABLE IF NOT EXISTS wordcloud_metadata (
discord_user_id BIGINT PRIMARY KEY,
tracked_since_message_channel_id BIGINT NOT NULL,
tracked_since_message_id BIGINT NOT NULL UNIQUE
)
""")
self.cursor.execute("""
await self.bot.db.execute("""
CREATE TABLE IF NOT EXISTS wordcloud_words (
discord_user_id BIGINT NOT NULL,
word TEXT NOT NULL,
@@ -83,18 +86,17 @@ class WordCloud(commands.Cog):
self.bot.logger.info("Unloading cog")
self.insert_cache_loop.cancel()
await self.insert_cache()
self.cursor.close()
def populate_consenting_users(self):
async def populate_consenting_users(self):
"""
Populates the cached list of consenting users
"""
self.cursor.execute("""
rows = await self.bot.db.fetchall("""
SELECT discord_user_id
FROM wordcloud_metadata
""")
self.consenting_users = [row[0] for row in self.cursor.fetchall()]
self.consenting_users = [row[0] for row in rows]
async def insert_cache(self):
"""
@@ -105,19 +107,21 @@ class WordCloud(commands.Cog):
if not self.word_freq_cache:
return
# Swap in an empty cache before awaiting, so words from messages sent in the meantime aren't lost
cache, self.word_freq_cache = self.word_freq_cache, defaultdict(lambda: defaultdict(int))
# Flatten the defaultdict into a list of tuples
# This is a bit of a hack but it works
word_freqs = list(
itertools.chain.from_iterable(
[(user_id, word, freq) for word, freq in user_word_freqs.items()]
for user_id, user_word_freqs in self.word_freq_cache.items()
for user_id, user_word_freqs in cache.items()
)
)
# Insert cache into database
try:
execute_batch(
self.cursor,
await self.bot.db.executemany(
"""
INSERT INTO wordcloud_words (discord_user_id, word, frequency)
VALUES (%s, %s, %s)
@@ -126,11 +130,9 @@ class WordCloud(commands.Cog):
""",
word_freqs,
)
except psycopg2.Error as err:
except psycopg.Error as err:
self.bot.logger.error(f"Failed to insert wordcloud cache into database - {err}")
self.word_freq_cache.clear()
@tasks.loop(minutes=20)
@misc_utils.log_errors
async def insert_cache_loop(self):
@@ -323,15 +325,14 @@ class WordCloud(commands.Cog):
)
try:
self.cursor.execute(
await self.bot.db.execute(
"""
INSERT INTO wordcloud_metadata
VALUES (%s, %s, %s)
""",
(interaction.user.id, interaction.channel_id, interaction.id),
)
except psycopg2.Error as err:
self.bot.db_connection.rollback()
except psycopg.Error as err:
self.bot.logger.error(f"Failed to insert wordcloud metadata into database - {err}")
return await interaction.response.send_message(
embed=embed_templates.error_fatal("Klarte ikke å skrive til database"),
@@ -365,15 +366,13 @@ class WordCloud(commands.Cog):
self.word_freq_cache.pop(interaction.user.id, None)
try:
self.cursor.execute(
"""
DELETE FROM wordcloud_metadata WHERE discord_user_id = %s;
DELETE FROM wordcloud_words WHERE discord_user_id = %s;
""",
(interaction.user.id, interaction.user.id),
await self.bot.db.execute_in_transaction(
[
("DELETE FROM wordcloud_metadata WHERE discord_user_id = %s", (interaction.user.id,)),
("DELETE FROM wordcloud_words WHERE discord_user_id = %s", (interaction.user.id,)),
]
)
except psycopg2.Error as err:
self.bot.db_connection.rollback()
except psycopg.Error as err:
self.bot.logger.error(f"Failed to delete wordcloud metadata from database - {err}")
return await interaction.response.send_message(
embed=embed_templates.error_fatal("Klarte ikke å slette fra database"),
@@ -395,7 +394,7 @@ class WordCloud(commands.Cog):
interaction (discord.Interaction): Slash command context object
"""
self.cursor.execute(
result = await self.bot.db.fetchall(
"""
SELECT word, frequency
FROM wordcloud_words
@@ -404,7 +403,6 @@ class WordCloud(commands.Cog):
""",
(interaction.user.id,),
)
result = self.cursor.fetchall()
if not result:
return await interaction.response.send_message(
@@ -442,7 +440,7 @@ class WordCloud(commands.Cog):
await self.insert_cache()
# Fetch word count from database
self.cursor.execute(
results = await self.bot.db.fetchall(
"""
SELECT word, frequency
FROM wordcloud_words
@@ -450,7 +448,6 @@ class WordCloud(commands.Cog):
""",
(interaction.user.id,),
)
results = self.cursor.fetchall()
if not results:
return await interaction.followup.send(
@@ -458,7 +455,7 @@ class WordCloud(commands.Cog):
)
# The stored "message" ID is the ID of the consent interaction, so its timestamp is when tracking started
self.cursor.execute(
row = await self.bot.db.fetchone(
"""
SELECT tracked_since_message_id
FROM wordcloud_metadata
@@ -466,7 +463,7 @@ class WordCloud(commands.Cog):
""",
(interaction.user.id,),
)
tracked_since = discord.utils.snowflake_time(self.cursor.fetchone()[0])
tracked_since = discord.utils.snowflake_time(row[0])
generation_task = functools.partial(WordCloud.generate_wordcloud_from_frequencies, dict(results))
word_cloud = await self.bot.loop.run_in_executor(None, generation_task)
+29 -7
View File
@@ -6,10 +6,11 @@ from zoneinfo import ZoneInfo
import aiohttp
import discord
import psycopg2
import yaml
from discord.ext import commands
from psycopg_pool import AsyncConnectionPool
from cogs.utils.database import Database
from logger import BotLogger
DATABASE_RELIANT_COGS = {
@@ -48,14 +49,23 @@ class Bot(commands.Bot):
self.cog_files = set(listdir("./src/cogs"))
# Check for missing credentials
self.db_pool = None
if self.check_credentials(config["database"], DATABASE_RELIANT_COGS):
self.db_connection = psycopg2.connect(
host=config["database"]["host"],
dbname=config["database"]["dbname"],
user=config["database"]["username"],
password=config["database"]["password"],
self.db_pool = AsyncConnectionPool(
kwargs={
"host": config["database"]["host"],
"dbname": config["database"]["dbname"],
"user": config["database"]["username"],
"password": config["database"]["password"],
"autocommit": True,
},
configure=self.configure_db_connection,
check=AsyncConnectionPool.check_connection,
min_size=1,
max_size=5,
open=False,
)
self.db_connection.autocommit = True # Scary
self.db = Database(self.db_pool)
if self.check_credentials(config["sanity"], SANITY_RELIANT_COGS):
self.sanity = config["sanity"]
@@ -81,6 +91,8 @@ class Bot(commands.Bot):
async def setup_hook(self):
self.http_session = aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=15))
if self.db_pool:
await self.db_pool.open(wait=True, timeout=30)
# Docker stops the container with SIGTERM. Close gracefully so cogs can save their caches
try:
@@ -105,6 +117,16 @@ class Bot(commands.Bot):
await super().close()
if hasattr(self, "http_session"):
await self.http_session.close()
if self.db_pool:
await self.db_pool.close()
@staticmethod
async def configure_db_connection(conn):
"""
Sets up every new database connection the same way the old single connection was set up
"""
await conn.execute("SET timezone TO 'Europe/Oslo'")
def check_credentials(self, credentials: dict, dependant_cogs: set[str]) -> bool:
"""
Generated
+80 -49
View File
@@ -1007,7 +1007,8 @@ dependencies = [
{ name = "opencv-python" },
{ name = "pillow" },
{ name = "psutil" },
{ name = "psycopg2-binary" },
{ name = "psycopg", extra = ["binary"] },
{ name = "psycopg-pool" },
{ name = "pypandoc" },
{ name = "pyyaml" },
{ name = "regex" },
@@ -1035,7 +1036,8 @@ requires-dist = [
{ name = "opencv-python", specifier = "==5.0.*" },
{ name = "pillow", specifier = ">=9.2,<12.0" },
{ name = "psutil", specifier = "==7.2.*" },
{ name = "psycopg2-binary", specifier = "==2.9.*" },
{ name = "psycopg", extras = ["binary"], specifier = "==3.3.*" },
{ name = "psycopg-pool", specifier = "==3.3.*" },
{ name = "pypandoc", specifier = "==1.17.*" },
{ name = "pyyaml", specifier = "==6.0.*" },
{ name = "regex", specifier = "==2026.9.29" },
@@ -1416,55 +1418,84 @@ wheels = [
]
[[package]]
name = "psycopg2-binary"
version = "2.9.13"
name = "psycopg"
version = "3.3.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ed/76/7b4383014be0fcc6c1c0e24292845a14e1672cf17fca62ca0a2bd5f4563d/psycopg2_binary-2.9.13.tar.gz", hash = "sha256:e324ecf60f952d21dd11413b8bbed0951bbd99579a06fd06f28bfc37737cd373", size = 378112, upload-time = "2026-09-10T00:06:12.199Z" }
dependencies = [
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
{ name = "tzdata", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/76/26/3ea4ca5eaea1c0debcdf7ee7c1613fbe721dc27a03c461c0817ffd8a0601/psycopg-3.3.6.tar.gz", hash = "sha256:c081f2250df751a943036e42db6df4571c66cd0aabe8291a7a506512b12007d2", size = 168171, upload-time = "2026-09-18T13:22:55.152Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/d1/d0125c56b865e3bc9f318d84930b2df71a729229dbb0ce12de748a82a6d7/psycopg2_binary-2.9.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2bf9f97a6df69a5d89d054b8cf5257a0916096c479800715fbfe7974dbcb3a26", size = 3725735, upload-time = "2026-09-09T23:54:51.182Z" },
{ url = "https://files.pythonhosted.org/packages/54/a5/b5a73d0910555e38ee12c49c1740855f8a1e9776e87d65f0c51e1bab762a/psycopg2_binary-2.9.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:07b7bd9f410650c34c3532162cc329f112368d78a3fc8668cb1ea9df61bc11bf", size = 3818000, upload-time = "2026-09-09T23:54:53.229Z" },
{ url = "https://files.pythonhosted.org/packages/3d/43/3e4783f62ae3f4fc19a5acf8d1c394df54458f1336febe188f317556d2a7/psycopg2_binary-2.9.13-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0463c00f946517f3e69192a59e6601e023ff9de45ad0a875eda3d6b1bebeb7ce", size = 4585760, upload-time = "2026-09-09T23:54:55.313Z" },
{ url = "https://files.pythonhosted.org/packages/8d/c4/a9a67ae65ad3d567eb0fc9cdf9a5a2783b779aecdcdc8945f1807b13d99e/psycopg2_binary-2.9.13-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e3861eba31f8ea8663fd876166b032fd89179e42aa63764d6feb281f13f9eb60", size = 4282380, upload-time = "2026-09-09T23:54:57.362Z" },
{ url = "https://files.pythonhosted.org/packages/b3/db/9d459d3da12e0b841cf1596579455aaa27e9e593e3e9a5a4ded5a55a7c15/psycopg2_binary-2.9.13-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3dc3372b3731b3ef23407fe06b94f640ef87a2bda242fa386033d5589c87514a", size = 5902039, upload-time = "2026-09-09T23:55:01.955Z" },
{ url = "https://files.pythonhosted.org/packages/d6/53/21079c10a581c50b6817498eda7c3481c1b3cdb41482bd08ebfccd3664c4/psycopg2_binary-2.9.13-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0405dd4d97720e7ab177aa02e493f524907c4cb3c445ac173e2627948d3d0528", size = 4119840, upload-time = "2026-09-09T23:55:04.336Z" },
{ url = "https://files.pythonhosted.org/packages/c4/ce/71e8d9e1b4f3e78157b49a5abdff50d915e95f2812550f6c9b4f2e4d5e94/psycopg2_binary-2.9.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b6ae51708201f501a171b02419d0c30878a743c369c9054eb1289f0f8d5979e2", size = 3661077, upload-time = "2026-09-09T23:55:06.118Z" },
{ url = "https://files.pythonhosted.org/packages/d9/54/b17616472f09a0fae96f8852692948b7eaa7c971d7629696da0e5932d996/psycopg2_binary-2.9.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:81682c227cc1849c4a6adf7b85274229073bb4c9d6ad5697222c695dcea5a8a7", size = 3307202, upload-time = "2026-09-09T23:55:08.061Z" },
{ url = "https://files.pythonhosted.org/packages/77/c7/d9737e222a377dac67a0ce0a2c73e7231a57f5cf18bb35a65d5c8d45d5d2/psycopg2_binary-2.9.13-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:13d955f6054a705a19554364fe9888d0a6e8b0746dc7ebc08a447c7b4fd4145c", size = 3052893, upload-time = "2026-09-09T23:55:10.209Z" },
{ url = "https://files.pythonhosted.org/packages/7d/3d/c406c9f698f518c264381192c2bdf8952ee84e469ffa9f82db1411f57385/psycopg2_binary-2.9.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7e2405196a8cfe6cd3e54172a54452dcf85c241eaf2e9dde7190d7469f7f5ef7", size = 3355210, upload-time = "2026-09-09T23:55:11.883Z" },
{ url = "https://files.pythonhosted.org/packages/27/64/6e3a96699770af2d0d49a2002f722c69b656fc27623ff89281cf2b109644/psycopg2_binary-2.9.13-cp312-cp312-win_amd64.whl", hash = "sha256:376ebf7d8aee4b7386b2bac31fdc27911e7e57cd0a88f1e038b8b149398ac008", size = 2767784, upload-time = "2026-09-09T23:55:13.823Z" },
{ url = "https://files.pythonhosted.org/packages/82/0a/795f2869788373cf7d08410341a444196e8ccebbac07a70a8f9a1f60e72f/psycopg2_binary-2.9.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4d66bfd44a46eb88cff0287929a4193fb45166b6c1f84bb1b233cc17ece0813c", size = 3725723, upload-time = "2026-09-09T23:55:15.887Z" },
{ url = "https://files.pythonhosted.org/packages/b5/63/5a9633f4563a73beba69b20a846ddd14c1c6ac072f5e8aab0da97ffabc2a/psycopg2_binary-2.9.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f818161d2302b3b3e9c75d5a1d0a5c5679e92e45cfec6432b9d5432dde5ff1f1", size = 3817976, upload-time = "2026-09-09T23:55:18.025Z" },
{ url = "https://files.pythonhosted.org/packages/6c/e2/b2e3b3a4331dc8b58e328cda30f3d0cc43a94b7aaf0c8383efd53dd10e95/psycopg2_binary-2.9.13-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:31db6cba66df5231dfd91d9f69188bec3fe6c8baae384e93a0ce792067ee2d98", size = 4585813, upload-time = "2026-09-09T23:55:20.112Z" },
{ url = "https://files.pythonhosted.org/packages/56/5c/87daea77c4132114d1a5da3a4928dd59446c3b3cc73d288cae08cf0b91a6/psycopg2_binary-2.9.13-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f04ada42bcd537adbaf8b7f3140237a204e452a88d0c1831cfce69f7d2e59f4e", size = 4282438, upload-time = "2026-09-09T23:55:22.329Z" },
{ url = "https://files.pythonhosted.org/packages/91/e5/56f9efdc9337acbd1a75798d97163183b63a1babc17602f7163009506c96/psycopg2_binary-2.9.13-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa37089795bd9701576edc2eb5849ce77a439eda9dfdfa47857449332cfa5292", size = 5902064, upload-time = "2026-09-09T23:55:24.37Z" },
{ url = "https://files.pythonhosted.org/packages/e4/15/f7ed0b90b47b73a9087306b42267eccfd919f92c0fb057e46bd2fa2efa4d/psycopg2_binary-2.9.13-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:41c2eb569ebd0e1b02d30d361a46932923b193fe1b5e641fb4d547c75e218955", size = 4119848, upload-time = "2026-09-09T23:55:26.433Z" },
{ url = "https://files.pythonhosted.org/packages/42/08/3091347b9fc5766e979aba6b0756ad14ce867a6bb245f3d69ac71fb768c6/psycopg2_binary-2.9.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f699a5225094a5c61402984e2fc1eca20e940223e76767c88189efb0c313f69", size = 3661129, upload-time = "2026-09-09T23:55:28.449Z" },
{ url = "https://files.pythonhosted.org/packages/34/c4/4f9a84d55484c9794b364548eb6e1fe10a57f123afd19729e5a1cc8ad7fc/psycopg2_binary-2.9.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5f04ae99c9fbb94c3197ec88599ed7db921f6adcddfe83687a74c7ead4037c22", size = 3307262, upload-time = "2026-09-09T23:55:30.384Z" },
{ url = "https://files.pythonhosted.org/packages/83/42/6eba8306a61dc890805ae475a9e71790a1c5461ccacbd4f0a1f3f57b40f0/psycopg2_binary-2.9.13-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:81404c37e0344ebcf10aac127d33d35137e5dbab1daf9f3deee46188fd5879c2", size = 3052898, upload-time = "2026-09-09T23:55:32.961Z" },
{ url = "https://files.pythonhosted.org/packages/b3/5d/42a8935ab280e8dcd7c07a655c0c3d25d62e9e242be1961ac14630f1294a/psycopg2_binary-2.9.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:feb7b1856f6ca805cc0e08739858f6cdfed8ce903390126af30343c62899a389", size = 3355265, upload-time = "2026-09-09T23:55:35.071Z" },
{ url = "https://files.pythonhosted.org/packages/87/c2/0e0ffb4caeb651631cbc6c8ead83e2a16457750b1d2eb7f5ef111c1f4d36/psycopg2_binary-2.9.13-cp313-cp313-win_amd64.whl", hash = "sha256:691da68ae5dd7c3ac77514357d35ece7b1ba8b5f3e6c92735198aa6159c355c8", size = 2767914, upload-time = "2026-09-09T23:55:37.14Z" },
{ url = "https://files.pythonhosted.org/packages/5f/32/897c074cb99fbdda7d34b0a2546097a59162bb3d04c0d546ae4ec82345e3/psycopg2_binary-2.9.13-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2ca263643ae37998ae04d18e431df34d0d61f12b47640dab585f14b6dbe00798", size = 3725788, upload-time = "2026-09-09T23:55:39.04Z" },
{ url = "https://files.pythonhosted.org/packages/0f/f4/e3a789de34c9ac25d20b25c2be583da16394a2ba0926da1c863653831f41/psycopg2_binary-2.9.13-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c0214c7da18a28d108aa7108c8a3cca8035c7911ec97ef9ec0827569c9a2720", size = 3818111, upload-time = "2026-09-09T23:55:40.979Z" },
{ url = "https://files.pythonhosted.org/packages/72/29/647724c43ac510dbc59b80e20e85d439deb94f5d5a024153c32330fa041d/psycopg2_binary-2.9.13-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5d89e064bb12b40cad696cf4975e6da86f8c60f14cd06cb6c1bc0a7f5d01761f", size = 4586235, upload-time = "2026-09-09T23:55:43.012Z" },
{ url = "https://files.pythonhosted.org/packages/91/ad/7f52f92cc65c23778daff7eec4ee2099236694a0a4723a5f180d0708b607/psycopg2_binary-2.9.13-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:190c18b97d9ef72f2e88c451b6588af90d6bd7bf54cb94b963280dc86a2c7076", size = 4282424, upload-time = "2026-09-09T23:55:44.843Z" },
{ url = "https://files.pythonhosted.org/packages/3d/2a/1a472059b198942d99651656e2bc610575584478bfe68d297ecabbd4887f/psycopg2_binary-2.9.13-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c00ebe9a2f31151aade0db233dc1446513a95e92c39ce055ee097af0ae86be1c", size = 5902365, upload-time = "2026-09-09T23:55:46.619Z" },
{ url = "https://files.pythonhosted.org/packages/91/1a/171ea5dac7b3a0fa57b3cb59c2ad6d7b8bc60732368fecfd2ed1f1288392/psycopg2_binary-2.9.13-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5085f7ff7b1e890f279577cedeb8c628957869a340fa34a39f7f406500b3c916", size = 4119938, upload-time = "2026-09-09T23:55:49.381Z" },
{ url = "https://files.pythonhosted.org/packages/41/ce/3c6d4ad71853a59eee6a575fe36df4bb40752a9735a27bd62af66b454ed5/psycopg2_binary-2.9.13-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4e55357d1943673d491bbabb171c891704fc6a22441fea539e05a5c27a79ea3c", size = 3661557, upload-time = "2026-09-09T23:55:51.269Z" },
{ url = "https://files.pythonhosted.org/packages/10/a3/1819a01bf951eab2afb5ca2a3d11f50500bf536fecff088154372a8d1985/psycopg2_binary-2.9.13-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3e60b06ec7f9dc3e5f1106d12706514b6d6b92c3dc438fcdf4e43e65cc660d1b", size = 3307770, upload-time = "2026-09-09T23:55:53.196Z" },
{ url = "https://files.pythonhosted.org/packages/4e/df/22f4aec952cd5b2dd02f438399583ed69f7d04b90e7c31659d9571bbe188/psycopg2_binary-2.9.13-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dde942b46ce20f6c4464cdf551f3293207f803f4e4354454eb1f5599c3eb1fa1", size = 3052935, upload-time = "2026-09-09T23:55:55.117Z" },
{ url = "https://files.pythonhosted.org/packages/95/42/aab651bc22bafa961806ca3b21027bb0739a2730b0e6f7f0778baeb95e67/psycopg2_binary-2.9.13-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:215777c62ce81c3b487cefdb6a41969944eb982309f91349ff3ca0323d6f17ed", size = 3355387, upload-time = "2026-09-09T23:55:57.366Z" },
{ url = "https://files.pythonhosted.org/packages/bc/af/3b8220633eaf955e95ea7be67d76e81a0d1cd3c76362ea504b91ffa079db/psycopg2_binary-2.9.13-cp314-cp314-win_amd64.whl", hash = "sha256:f3088eb80f58ed933c62d87128741d31e786edc862e23266d3c286763d646de0", size = 2863890, upload-time = "2026-09-09T23:55:59.056Z" },
{ url = "https://files.pythonhosted.org/packages/6e/f1/377d17fc8425220d17552691cd2b97aa232da92173f5dead71278b83f8ab/psycopg2_binary-2.9.13-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:38397def2d794ffde9db80f63d6820253e61b17483112652a318355f51a56f50", size = 3725913, upload-time = "2026-09-09T23:56:00.736Z" },
{ url = "https://files.pythonhosted.org/packages/67/64/27208e67cd6e663f69bf7bf905cf69db066a015c90ac9ca948a56a8e9d78/psycopg2_binary-2.9.13-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:dff5c70ed9789ccb0d97ff4a7da51dc523a255c4ec95df188fa5d44adcae4ea8", size = 3818303, upload-time = "2026-09-09T23:56:02.551Z" },
{ url = "https://files.pythonhosted.org/packages/6b/98/67d2f34a1d18367b5f655bdd101759f8474286c74ffe701b7d6e3abd7fda/psycopg2_binary-2.9.13-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:08d3b81a6a91775c937abf97d4c58fc9142e8e35fb91c387d24f81d15c98e6cf", size = 4586508, upload-time = "2026-09-09T23:56:04.706Z" },
{ url = "https://files.pythonhosted.org/packages/bb/47/46c227deaf322dceafa0b7b321b4e5de9cc797014b7a353349b2e09b1118/psycopg2_binary-2.9.13-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:541a487a9ccd72b5e38f37f27b0ce78cb7eb3e336e7b5277d45463010c03a7a8", size = 4282383, upload-time = "2026-09-09T23:56:06.678Z" },
{ url = "https://files.pythonhosted.org/packages/f4/3c/e8705ffa381160d842eaf06a8446e8416f1a2497dd70a7e62277f3be6e7a/psycopg2_binary-2.9.13-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:562fe2a43b30e781848dce63d9080c15414c777c96df348c4342558338cc7bf3", size = 5902765, upload-time = "2026-09-09T23:56:08.634Z" },
{ url = "https://files.pythonhosted.org/packages/53/cc/359821c18317228b8032456a3740c98045b719ed003a594b9ebac9330b86/psycopg2_binary-2.9.13-cp315-cp315-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dddfe650e7dda464d676c27fbedb5061f1ad05e1604627f54c770d7f799d36e9", size = 4122849, upload-time = "2026-09-09T23:56:10.671Z" },
{ url = "https://files.pythonhosted.org/packages/17/e5/4d935acb6d3258c7a767b3d527e54c0b537649101b55002a5dbcfe747e2a/psycopg2_binary-2.9.13-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:4ff0f575cbb14f30445858dcfdd751e043486f5290915df78a9818bc74042eff", size = 3662084, upload-time = "2026-09-09T23:56:12.316Z" },
{ url = "https://files.pythonhosted.org/packages/89/56/9e9bbc7c773c5de7bb25dd35d7f041c2a6f0fcfa9207a1ceaf01a1bc687c/psycopg2_binary-2.9.13-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d79530b4c1af657d5620a1d21b8e39f2996aa06821d5564d05b22d6b8cd413d0", size = 3308077, upload-time = "2026-09-09T23:56:15.262Z" },
{ url = "https://files.pythonhosted.org/packages/36/fa/ed742cd4e5dbddcb44702f9c4a97f7f5b62d97e3d9d00907ecc8ac750ef4/psycopg2_binary-2.9.13-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:6ede8595767e19d30a7e8a84a7d47bfde6176d45d194fed08dbb68d1584a780b", size = 3055143, upload-time = "2026-09-09T23:56:17.168Z" },
{ url = "https://files.pythonhosted.org/packages/d5/3a/5c2cb71a844ee236be2ce91b286d797e34a21489909357c7cfba0f5c0197/psycopg2_binary-2.9.13-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:0ebcf3c4266a695df9d0ef51296155f60c86ac51cf82f0d0dd2e827255a891c5", size = 3355374, upload-time = "2026-09-09T23:56:18.793Z" },
{ url = "https://files.pythonhosted.org/packages/e8/30/3991c9fdcca90a5a1e55435292f4d74d176da2be15f3998f6858da3658cc/psycopg2_binary-2.9.13-cp315-cp315-win_amd64.whl", hash = "sha256:1752b9821f1377404d65ac43af03d59a1eccc57fb2c1eb8305f9a3fe8eb7a8ba", size = 2867246, upload-time = "2026-09-09T23:56:20.501Z" },
{ url = "https://files.pythonhosted.org/packages/4e/de/748bd7609c71cae5d737f0ba9192f19329f70180ecda8fff3cac02c5abe3/psycopg-3.3.6-py3-none-any.whl", hash = "sha256:a1db9f7148b06a28606767efaca51fa6f9398c5c0a3810519be69d7000bdb631", size = 215490, upload-time = "2026-09-18T13:15:29.374Z" },
]
[package.optional-dependencies]
binary = [
{ name = "psycopg-binary", marker = "implementation_name != 'pypy'" },
]
[[package]]
name = "psycopg-binary"
version = "3.3.6"
source = { registry = "https://pypi.org/simple" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e6/01/2cdd1824e58b4467ee0b9498664cd28c42d8794db6b1e35b6bcb834f0044/psycopg_binary-3.3.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3f84dab25e0385692ee13274c68678377e0b1a70ab9d14e56264cbf61f60c62d", size = 4707086, upload-time = "2026-09-18T13:18:05.138Z" },
{ url = "https://files.pythonhosted.org/packages/f6/76/de9948ac06895261c84d5b9fbe283d8f3c5bc9f070691b8d9eaa1b51e322/psycopg_binary-3.3.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:612382ac3ed13651c7fa44b5fee9fbf7baaa2ddbc6f500391672682c5f1df9e0", size = 4769607, upload-time = "2026-09-18T13:18:12.83Z" },
{ url = "https://files.pythonhosted.org/packages/76/a9/72436c9915ee4905964689e7f0e182ce7767cc0a0390b3ce703be8177625/psycopg_binary-3.3.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:366db6e97e66b37211475f20c4c1324a2dc0dd825e46d4e87f9d599304d276f9", size = 5554134, upload-time = "2026-09-18T13:18:21.175Z" },
{ url = "https://files.pythonhosted.org/packages/0a/42/948bb3d2617795093512613fd96ba380e922992c7908fbc073858147d196/psycopg_binary-3.3.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1679a1cb93fbe5a6d1fd58d82cbddcc6fcb8c61446ba7cae6eb2a7b19bc585de", size = 5235723, upload-time = "2026-09-18T13:18:27.071Z" },
{ url = "https://files.pythonhosted.org/packages/99/47/93e823ff1b0088400703410939c9bda3e63ed9c850b3ee088e8769f4c10b/psycopg_binary-3.3.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37d40450659401600e6d043ff586c89a71a69f33cbb8bcdba6cdb2569beecdbe", size = 6833587, upload-time = "2026-09-18T13:18:33.794Z" },
{ url = "https://files.pythonhosted.org/packages/5e/2d/ecc69c847795aa704041a9f5667a6b0938a088cf1853636d762a6938e493/psycopg_binary-3.3.6-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a5165300324efd5a772c48a88ab3a928513ab3979fca76553e62ee815f7b2b9c", size = 5070013, upload-time = "2026-09-18T13:18:39.628Z" },
{ url = "https://files.pythonhosted.org/packages/92/36/6126f0dac21713dcae91404f2a76da18598a6252339a8c669c46370d43b2/psycopg_binary-3.3.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d636338c8f21b0df2f84657b00bc34f9313f826ef93f1155bc743607e4a0c5eb", size = 4597367, upload-time = "2026-09-18T13:18:45.023Z" },
{ url = "https://files.pythonhosted.org/packages/4d/29/7ecfc04243b46c89ffd49924e9c5634ea904ef96c7d0f37e4073623584c1/psycopg_binary-3.3.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a4ee3bdd5468a725f2a4d9aab8a74b6d0279f768c8b5d3aeb102c5307ff3d59c", size = 4275419, upload-time = "2026-09-18T13:18:49.299Z" },
{ url = "https://files.pythonhosted.org/packages/6e/90/2f46d2e0de79706ac170df0a3637fe63c4498fc04f131f6049520b78b806/psycopg_binary-3.3.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:289aadd6a00e151203c081f708348ec89f1e483c9b510ef4ac3981f847f01f79", size = 4007358, upload-time = "2026-09-18T13:18:53.944Z" },
{ url = "https://files.pythonhosted.org/packages/03/48/6744e91291b751a8cf12d63d719977974bb94c84ceba913e7ddb2e478e51/psycopg_binary-3.3.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f21d057f3e5f5491067e5b292498073b73847d48799b099803fef100775fcc52", size = 4320156, upload-time = "2026-09-18T13:18:59.258Z" },
{ url = "https://files.pythonhosted.org/packages/1a/9b/94ff7fce53a64d5b286e2ec454e0a025cf3d6e6b4a9189bef16aa5de98b2/psycopg_binary-3.3.6-cp312-cp312-win_amd64.whl", hash = "sha256:e23a66a763fbe83fcc210bc77c27e5a5ea380ebf091c06f34d8561b695e5a40f", size = 3658864, upload-time = "2026-09-18T13:19:06.503Z" },
{ url = "https://files.pythonhosted.org/packages/b4/c3/c072584b69ad44a747b448cfc9766fecb8aae56e372a017e2ef668790057/psycopg_binary-3.3.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5ad8f35e67cc16d1fad1fa8c88972dc9b3a3141ea67897399904edab96a301b6", size = 4712284, upload-time = "2026-09-18T13:19:13.451Z" },
{ url = "https://files.pythonhosted.org/packages/0a/b9/4283b785339e8e2318d03048994b093d650ea6289fabaa806b765dc0d449/psycopg_binary-3.3.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:373704aea331d3f3e3402c125a1543f5875e2986ebb54f97d1647942161f803f", size = 4772031, upload-time = "2026-09-18T13:19:18.524Z" },
{ url = "https://files.pythonhosted.org/packages/6f/72/7a1321d359246769fff1affffbd0132785a28f7f63c18524c15a502398f4/psycopg_binary-3.3.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b82491019b884d62318b5f30706c3d7e6d4e5a6cb7eabcb3edc0c1b0fdaceae9", size = 5556392, upload-time = "2026-09-18T13:19:24.418Z" },
{ url = "https://files.pythonhosted.org/packages/de/b0/c6f8a0585a5dacbea74e130bcfc66629390e8f5bbc79d2a8e806e8952150/psycopg_binary-3.3.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cec5ea900390897d0b46130f60bc2883bf19c314f9044235217c8be88b0ef269", size = 5237855, upload-time = "2026-09-18T13:19:31.257Z" },
{ url = "https://files.pythonhosted.org/packages/e2/fc/c3a7a8bbef7e945ec584ac61d460a612363ea398511cd0e220242b1d69f1/psycopg_binary-3.3.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98c02090d88f2ebc0ec1e8da538f77d225ce0fffecf372aa39262e62a1b054ef", size = 6833856, upload-time = "2026-09-18T13:19:43.622Z" },
{ url = "https://files.pythonhosted.org/packages/a9/f2/8e80b921db728ebb68fc105bd7c4277f908210ad755bd6481d5ea7add740/psycopg_binary-3.3.6-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ee2c4728c691245e24501fcd7a97b5b381236b9985bc445bba88cdce7d1b5784", size = 5070730, upload-time = "2026-09-18T13:19:49.968Z" },
{ url = "https://files.pythonhosted.org/packages/54/6a/5b313e0c5348244f0e973aff3258bf86766656256d5ece8d541a53e35b4a/psycopg_binary-3.3.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f19cc87343eaa55255e76b31259a570072ac95d6ae82c92dd34b97691f5e49dc", size = 4598089, upload-time = "2026-09-18T13:19:56.426Z" },
{ url = "https://files.pythonhosted.org/packages/32/e9/db7f76ec24bf6699e92bf604e5c4bae10664a681a8999ef42aa0faf0f2c6/psycopg_binary-3.3.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdccb3a0e184b03e9baa673b15a809cf36c339c85dbda0ebc25a698846dfbee8", size = 4278481, upload-time = "2026-09-18T13:20:04.681Z" },
{ url = "https://files.pythonhosted.org/packages/61/83/72c67013656f4d6b547caabffb193e91d57e63f90eefdcc6d045c400e97d/psycopg_binary-3.3.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9892188bb15e5803beb51afe8a25add6b56be391a53058e8bca03b74e1e6bf22", size = 4009229, upload-time = "2026-09-18T13:20:11.905Z" },
{ url = "https://files.pythonhosted.org/packages/82/35/5e4500df2c999eb0faed8b184e6958b834172128274f06167a5deef4c19c/psycopg_binary-3.3.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3af90f92769d8cc10f94515ee7a0aef36ea85ca733a0ce22858f6e0953f41138", size = 4321467, upload-time = "2026-09-18T13:20:17.949Z" },
{ url = "https://files.pythonhosted.org/packages/55/7f/e350e1cf498ba2565c3f87b12f429d2012eb86b76c2b3845a19ee5fbb4d6/psycopg_binary-3.3.6-cp313-cp313-win_amd64.whl", hash = "sha256:0ebfad5d131de9f892ae9e70cc7616207768b6714b66a52d4612b8ceaf78b372", size = 3658179, upload-time = "2026-09-18T13:20:22.691Z" },
{ url = "https://files.pythonhosted.org/packages/6d/b9/60711317c284a442511644ea7185b56ebe627606d6741e732cd16108c47b/psycopg_binary-3.3.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:b3f75dee0f9afafabe4edc52c4842f1e1878ed2069bd05b22d6fe961e97e4dba", size = 4720512, upload-time = "2026-09-18T13:20:29.278Z" },
{ url = "https://files.pythonhosted.org/packages/63/da/28befc84454cbc6374550de7746f591f8fe1b6165c1fce249652cc8291c4/psycopg_binary-3.3.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5927b7ba63153cd8e9862987290a2b783a5c590daf2a4ef981700cc3569166d4", size = 4782318, upload-time = "2026-09-18T13:20:35.401Z" },
{ url = "https://files.pythonhosted.org/packages/a4/8a/0d21c2c833cdc0d4244c77e858e0ed37fa2abec2623be4fd686f617109ce/psycopg_binary-3.3.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:0bf08b749cc144f33b44a91b78e3f71c60eb07963746a0df5a100b36ce3d7475", size = 5567460, upload-time = "2026-09-18T13:20:41.902Z" },
{ url = "https://files.pythonhosted.org/packages/49/6d/7692d0d4e656b6cc9868d8acc2e3b42f17a0db4a625400a6d093cb0533a1/psycopg_binary-3.3.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:31cd942c23f613276b81a6e6598cefa12960058b0f46e1e874b540c793f6aca5", size = 5246902, upload-time = "2026-09-18T13:20:47.661Z" },
{ url = "https://files.pythonhosted.org/packages/d4/c1/b8a1f18fb1b7558a17f57f7cb3fc8bc93189feea2958925950b3acb15743/psycopg_binary-3.3.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4690cf67738f0e0e49a32aeec99bf0e4595cc2b4f1af984a4345394b1dcff91a", size = 6847192, upload-time = "2026-09-18T13:20:56.874Z" },
{ url = "https://files.pythonhosted.org/packages/a5/76/404f33519167c65cca88ec4998776f1dbebccc301ee977f0e62c47fb0826/psycopg_binary-3.3.6-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ad1c785e784cfd87e8436c6b7702f2d321fc39601bbaf29bc63a41a867091638", size = 5079573, upload-time = "2026-09-18T13:21:04.155Z" },
{ url = "https://files.pythonhosted.org/packages/f0/d9/79e8fbc8f37262a415f3550f0bcc5f98037442bf3d12ef6cbae2056655ae/psycopg_binary-3.3.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:79a2a1c3449f6c3409427078ed1cec10de79f3023cb5f2504f0597d350ad46c7", size = 4613633, upload-time = "2026-09-18T13:21:10.664Z" },
{ url = "https://files.pythonhosted.org/packages/d4/47/96225db74be7d2ce04b3a58678b53cda610225055edf5faa775c9f501d8b/psycopg_binary-3.3.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:86147cb5d140341c3363fb5bacce31f8d5543902a46699d3c536b101bbceaf9e", size = 4293375, upload-time = "2026-09-18T13:21:16.027Z" },
{ url = "https://files.pythonhosted.org/packages/2a/d2/18e9c779a5efd565250329adaf529ecc2b8b2ed5be5cb0f6ccee208cbfd9/psycopg_binary-3.3.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7308c93cf0b19bbaf8e6ff0a6ad50d3c442385739245fe15a8d593bf841734a6", size = 4019883, upload-time = "2026-09-18T13:21:21.587Z" },
{ url = "https://files.pythonhosted.org/packages/ef/28/0cc654afc6c2cda982767f5679d3646b30b1ec86545bdaa9402202d6776c/psycopg_binary-3.3.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:05a83ac9fd52b9bca7cb5ab04b3691163170bd16f53defa27216ea3aa07ee781", size = 4332607, upload-time = "2026-09-18T13:21:27.63Z" },
{ url = "https://files.pythonhosted.org/packages/f1/3e/0a753a74fbd7aef120f286c016e09d3cc3f1daf7688f4a145d27281260b2/psycopg_binary-3.3.6-cp314-cp314-win_amd64.whl", hash = "sha256:1fbd30e537dab22cafdf080608f10148fe2a5f3a61294ddb5113caac8a623840", size = 3755671, upload-time = "2026-09-18T13:21:33.855Z" },
{ url = "https://files.pythonhosted.org/packages/0e/b1/a372b9c02aea50148e71c9853e19efca8fa5ae2010a8e27243b9b8f790c0/psycopg_binary-3.3.6-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:bf8c8481d026b85dd70c5fa7dde85b2333aed0b32a2602bcd38a900cbd78a49c", size = 4719571, upload-time = "2026-09-18T13:21:41.437Z" },
{ url = "https://files.pythonhosted.org/packages/65/7c/811e3828c6b82e2f10c6c9cdd963cfc66f3e024026e5a69ac18530bad984/psycopg_binary-3.3.6-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:b599defe9190b17e9907c8b4d114c181e702c87efcd1b8a0ad40971cdcc4634a", size = 4781230, upload-time = "2026-09-18T13:21:49.516Z" },
{ url = "https://files.pythonhosted.org/packages/3e/15/9a784eed813ea9e97c294af3ead63d02b7b203502c66380336c50065e441/psycopg_binary-3.3.6-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b8ece331509f7a975b90501f41e83ad905e4141753fedf3f2711b2bc70a8efbc", size = 5566111, upload-time = "2026-09-18T13:21:58.089Z" },
{ url = "https://files.pythonhosted.org/packages/68/16/47194e002007c27337b11e49bf459c4b19727463f9aff2e1a90917bcc806/psycopg_binary-3.3.6-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c61617eaae0112ca154da87ffb99b73af2c74067acac28dfb9a4455b019dff2e", size = 5249963, upload-time = "2026-09-18T13:22:06.695Z" },
{ url = "https://files.pythonhosted.org/packages/53/84/5dcf9f310b11f0675cd860c6b2c70f58ce61798a3ee3f6f962b53fa358ca/psycopg_binary-3.3.6-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c6d19cb4999d03231e8730a5f66c8f5068bc3b532677eb39dab0f600bff3e312", size = 6847925, upload-time = "2026-09-18T13:22:13.088Z" },
{ url = "https://files.pythonhosted.org/packages/f3/06/1957a06dc22963c418c27b284929579de84f29c37ad1abe6dc6ee9e8cf25/psycopg_binary-3.3.6-cp315-cp315-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8cbb54454dbf1bbf2ff08dd7693e8d94ac94b1a20f70f4b3b813d52ecb5cbc1", size = 5087720, upload-time = "2026-09-18T13:22:17.959Z" },
{ url = "https://files.pythonhosted.org/packages/21/43/ac07d042bae99b57bf123bb473632f29af544008094da0ffd285ab8011e2/psycopg_binary-3.3.6-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:dc75da5a20951049f7b773145f998f69d181adad9c58a0ff36e0cf1d73c10e10", size = 4613412, upload-time = "2026-09-18T13:22:26.719Z" },
{ url = "https://files.pythonhosted.org/packages/aa/b1/019156fbeafcefb4cccc9d109de4699493bceb8313c7545c8349e089dfbc/psycopg_binary-3.3.6-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:955e3dd94da361e052d2e49acf591017158dc8f8ed2c8a42c2e3943403c39dc2", size = 4292618, upload-time = "2026-09-18T13:22:33.042Z" },
{ url = "https://files.pythonhosted.org/packages/5d/0f/62113dc6b1df65983a1f2fc816c04b1edfa22f2ae9d4abee74ed267f4a96/psycopg_binary-3.3.6-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:c7753871eb57e6a5f4646f6168590c6653073dea5e9e720b201c8875332df4c8", size = 4027121, upload-time = "2026-09-18T13:22:38.334Z" },
{ url = "https://files.pythonhosted.org/packages/5d/d5/cf0cbd1ea5a7d8167fe2c6953efde19101f7b193bd61a23e6d622ad6854c/psycopg_binary-3.3.6-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:303732e798fe6729f8e12021b9c96107df8e95ecec4dd487c67b98ec2a59435e", size = 4336388, upload-time = "2026-09-18T13:22:45.576Z" },
{ url = "https://files.pythonhosted.org/packages/98/33/e2a5b36edf8aa422f6fa4b894756eb33dc93b36df5f65121280bb8b929c4/psycopg_binary-3.3.6-cp315-cp315-win_amd64.whl", hash = "sha256:2f122603f36050937982abf9668d8bc4769a79f7c93a65013b1c49f1cab7b56b", size = 3756154, upload-time = "2026-09-18T13:22:51.283Z" },
]
[[package]]
name = "psycopg-pool"
version = "3.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/74/5e/c0664b968b102ff68b811d999c728546c48d5c1eec03e3bbaf88c0cb4472/psycopg_pool-3.3.3.tar.gz", hash = "sha256:df87b5d9d0ad7db37f6cdad4fa8ce113d250f5997f6db38e9a99192fb67f9e1d", size = 32006, upload-time = "2026-09-22T15:53:24.947Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5d/b4/452c6607a0f479465cd8a9b0d9956919fcb150050c1f83f9f11e6b8ee8dc/psycopg_pool-3.3.3-py3-none-any.whl", hash = "sha256:9b9cd6a4fcec47a410f7e82d408540e7f77b478509e91b44c1a5457a13e5ff37", size = 40304, upload-time = "2026-09-22T15:53:23.712Z" },
]
[[package]]