From 01bccceb6dc13566b17749398c3d15f2dfdcaecf Mon Sep 17 00:00:00 2001 From: Leander Furumo Date: Sat, 24 Oct 2026 05:16:24 +0200 Subject: [PATCH] reaplce shared psycopg2 connection with psycopg3 async pool --- pyproject.toml | 3 +- src/cogs/birthday.py | 47 ++++++------- src/cogs/gullkorn.py | 33 ++++----- src/cogs/mc_whitelist.py | 26 ++++--- src/cogs/social_credit.py | 30 ++++---- src/cogs/starboard_scraper.py | 37 +++++----- src/cogs/streak.py | 70 +++++++++--------- src/cogs/user_facts.py | 39 +++++----- src/cogs/utils/database.py | 97 +++++++++++++++++++++++++ src/cogs/word_cloud.py | 71 +++++++++---------- src/run.py | 36 ++++++++-- uv.lock | 129 +++++++++++++++++++++------------- 12 files changed, 390 insertions(+), 228 deletions(-) create mode 100644 src/cogs/utils/database.py diff --git a/pyproject.toml b/pyproject.toml index ff974df..dbb370a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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", diff --git a/src/cogs/birthday.py b/src/cogs/birthday.py index 23bb757..976aaf1 100644 --- a/src/cogs/birthday.py +++ b/src/cogs/birthday.py @@ -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")) diff --git a/src/cogs/gullkorn.py b/src/cogs/gullkorn.py index f5e5598..69b1c22 100644 --- a/src/cogs/gullkorn.py +++ b/src/cogs/gullkorn.py @@ -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) diff --git a/src/cogs/mc_whitelist.py b/src/cogs/mc_whitelist.py index 656cb96..ece4227 100644 --- a/src/cogs/mc_whitelist.py +++ b/src/cogs/mc_whitelist.py @@ -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 diff --git a/src/cogs/social_credit.py b/src/cogs/social_credit.py index c40742e..7f88152 100644 --- a/src/cogs/social_credit.py +++ b/src/cogs/social_credit.py @@ -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( diff --git a/src/cogs/starboard_scraper.py b/src/cogs/starboard_scraper.py index 4c5cc45..7b8d844 100644 --- a/src/cogs/starboard_scraper.py +++ b/src/cogs/starboard_scraper.py @@ -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): diff --git a/src/cogs/streak.py b/src/cogs/streak.py index aa46f52..d670e3f 100644 --- a/src/cogs/streak.py +++ b/src/cogs/streak.py @@ -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")) diff --git a/src/cogs/user_facts.py b/src/cogs/user_facts.py index 2568088..fa445e8 100644 --- a/src/cogs/user_facts.py +++ b/src/cogs/user_facts.py @@ -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 :)") ) diff --git a/src/cogs/utils/database.py b/src/cogs/utils/database.py new file mode 100644 index 0000000..03e1f67 --- /dev/null +++ b/src/cogs/utils/database.py @@ -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() diff --git a/src/cogs/word_cloud.py b/src/cogs/word_cloud.py index 468dccd..bc12412 100644 --- a/src/cogs/word_cloud.py +++ b/src/cogs/word_cloud.py @@ -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) diff --git a/src/run.py b/src/run.py index 1c82766..b9421b6 100644 --- a/src/run.py +++ b/src/run.py @@ -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: """ diff --git a/uv.lock b/uv.lock index 4b3eace..fb81ddc 100644 --- a/uv.lock +++ b/uv.lock @@ -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]]