From d0afc23a22d69ed5a59e16e57c2a06a6b8dc86de Mon Sep 17 00:00:00 2001 From: HippoProgrammer <172101796+HippoProgrammer@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:31:22 +0000 Subject: [PATCH 1/4] Type hinting added for classes --- src/classes/auth.py | 31 ++++++++----------------------- src/classes/ifv.py | 8 +++----- src/classes/sse.py | 8 +++----- src/classes/wa.py | 8 +++----- 4 files changed, 17 insertions(+), 38 deletions(-) diff --git a/src/classes/auth.py b/src/classes/auth.py index cd76577..37a934c 100644 --- a/src/classes/auth.py +++ b/src/classes/auth.py @@ -18,40 +18,25 @@ from .exceptions import * import logging -class Permission: + +class DiscordObject: def __init__(self): self.initialized = False def fromAttributeValues(self, kind:str, identifier:int): self.kind = kind self.identifier = identifier self.initialized = True - return self - def fromSQLValues(self, values:tuple): + def fromSQLValues(self, values:tuple[str, int]): self.kind = values[0] self.identifier = values[1] self.initialized = True - return self - def toSQLValues(self): + def toSQLValues(self) -> tuple[str, int]: if self.initialized: return (self.kind, self.identifier) else: raise exceptions.UninitializedException() +class Permission(DiscordObject): + pass -class Channel: - def __init__(self): - self.initialized = False - def fromAttributeValues(self, kind:str, identifier:int): - self.kind = kind - self.identifier = identifier - self.initialized = True - return self - def fromSQLValues(self, values:tuple): - self.kind = values[0] - self.identifier = values[1] - self.initialized = True - return self - def toSQLValues(self): - if self.initialized: - return (self.kind, self.identifier) - else: - raise exceptions.UninitializedException() \ No newline at end of file +class Channel(DiscordObject): + pass \ No newline at end of file diff --git a/src/classes/ifv.py b/src/classes/ifv.py index dac33c2..7555da1 100644 --- a/src/classes/ifv.py +++ b/src/classes/ifv.py @@ -21,23 +21,21 @@ class IFV: def __init__(self): self.initialized = False - def fromAttributeValues(self, id:str, name:str, thread = None, ifvauthor = None, ifvlink = None): + def fromAttributeValues(self, id:str, name:str, thread:str | None = None, ifvauthor:str | None = None, ifvlink:str | None = None): self.id = id self.name = name self.thread = thread self.ifvauthor = ifvauthor self.ifvlink = ifvlink self.initialized = True - return self - def fromSQLValues(self, values:tuple): + def fromSQLValues(self, values:tuple[str, str, str | None, str | None, str | None]): self.id = values[0] self.name = values[1] self.thread = values[2] self.ifvauthor = values[3] self.ifvlink = values[4] self.initialized = True - return self - def toSQLValues(self): + def toSQLValues(self) -> tuple[str, str, str, str | None, str | None, str | None]: if self.initialized: return (self.id,self.name,self.thread,self.ifvauthor,self.ifvlink) else: diff --git a/src/classes/sse.py b/src/classes/sse.py index 8fe7778..ea00cfe 100644 --- a/src/classes/sse.py +++ b/src/classes/sse.py @@ -19,7 +19,7 @@ class Event: def __init__(self): self.initialized = False - def fromAttributeValues(self, event:int, time:int, category:str, data:list, actor = None, receptor = None, origin = None, destination = None): + def fromAttributeValues(self, event:int, time:int, category:str, data:list, actor: str | None = None, receptor:str | None = None, origin:str | None = None, destination:str | None = None): self.event = event self.time = time self.actor = actor @@ -29,8 +29,7 @@ def fromAttributeValues(self, event:int, time:int, category:str, data:list, acto self.category = category self.data = data self.initialized = True - return self - def fromSQLValues(self, values:tuple): + def fromSQLValues(self, values:tuple[int, int, str, list, str | None, str | None, str | None, str | None]): self.event = values[0] self.time = values[1] self.actor = values[2] @@ -40,8 +39,7 @@ def fromSQLValues(self, values:tuple): self.category = values[6] self.data = values[7] self.initialized = True - return self - def toSQLValues(self): + def toSQLValues(self) -> tuple[int, int, str, list, str | None, str | None, str | None, str | None]: if self.initialized: return (self.event,self.time,self.actor,self.receptor,self.origin,self.destination,self.category,self.data) else: diff --git a/src/classes/wa.py b/src/classes/wa.py index 4f5a833..1df2c9a 100644 --- a/src/classes/wa.py +++ b/src/classes/wa.py @@ -21,7 +21,7 @@ class Proposal: def __init__(self): self.initialized = False - def fromAttributeValues(self, id:str, council:int, name:str, category:str, author:str, legal:bool, quorum:bool, coauthors=[]): + def fromAttributeValues(self, id:str, council:int, name:str, category:str, author:str, legal:bool, quorum:bool, coauthors:list[str | None] = []): self.id = id self.council = council self.name = name @@ -31,8 +31,7 @@ def fromAttributeValues(self, id:str, council:int, name:str, category:str, autho self.legal = legal self.quorum = quorum self.initialized = True - return self - def fromSQLValues(self, values:tuple): + def fromSQLValues(self, values:tuple[str, int, str, str, str, bool, bool, list[str | None]]): self.id = values[0] self.council = values[1] self.name = values[2] @@ -42,8 +41,7 @@ def fromSQLValues(self, values:tuple): self.legal = values[6] self.quorum = values[7] self.initialized = True - return self - def toSQLValues(self): + def toSQLValues(self) -> tuple[str, int, str, str, str, bool, bool, list[str | None]]: if self.initialized: return (self.id,self.council,self.name,self.category,self.author,self.coauthors,self.legal,self.quorum) else: From 65634db8f968923b5f48469772a24ea63943f6f4 Mon Sep 17 00:00:00 2001 From: HippoProgrammer <172101796+HippoProgrammer@users.noreply.github.com> Date: Tue, 9 Jun 2026 13:33:24 +0000 Subject: [PATCH 2/4] Type hinting added to db, excepting list internal types --- src/customio/db.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/customio/db.py b/src/customio/db.py index a26c229..4c25c0a 100644 --- a/src/customio/db.py +++ b/src/customio/db.py @@ -69,7 +69,7 @@ async def setup_all(self) -> None: self.connection_self.connection_pool.check() async def cleanup(self) -> None: await self._close_connection_pool() - async def listen_for_new_sse_events(self,callback) -> None: + async def listen_for_new_sse_events(self,callback:function) -> None: """Add a listener that calls callback on all new SSE events""" try: async with self.connection_pool.connection() as conn: @@ -151,7 +151,7 @@ async def nsqueue_get_by_id(self, id:str) -> classes.wa.Proposal: return proposal except psycopg_pool.PoolTimeout: self.connection_self.connection_pool.check() - async def nsqueue_get_all_legal_by_council_limited(self, council = 1, limit = 7) -> list: + async def nsqueue_get_all_legal_by_council_limited(self, council:int = 1, limit:int = 7) -> list: """Get all proposals that are legal from the NSQueue, up to the specified limit""" try: async with self.connection_pool.connection() as conn: # get a connection from the pool @@ -254,7 +254,7 @@ async def ifvqueue_get_by_author(self, author:int) -> list: return ifvs except psycopg_pool.PoolTimeout: self.connection_self.connection_pool.check() - async def ifvqueue_get_unauthored_limited(self,limit = 7) -> list: + async def ifvqueue_get_unauthored_limited(self,limit:int = 7) -> list: """Get all IFVs from the IFVQueue with no author""" try: async with self.connection_pool.connection() as conn: # get a connection from the pool @@ -308,7 +308,7 @@ async def ifvqueue_update_link_by_id(self, id:str, link:str) -> None: await conn.commit() except psycopg_pool.PoolTimeout: self.connection_self.connection_pool.check() - async def ifvqueue_remove_author_link(self, id:str): + async def ifvqueue_remove_author_link(self, id:str) -> None: try: async with self.connection_pool.connection() as conn: # get a connection from the pool self.logger.debug('DB connection opened from pool') @@ -323,7 +323,7 @@ async def ifvqueue_remove_author_link(self, id:str): except psycopg_pool.PoolTimeout: self.connection_self.connection_pool.check() # BotPerms table - async def botperms_add(self, permission:classes.auth.Permission): + async def botperms_add(self, permission:classes.auth.Permission) -> None: try: async with self.connection_pool.connection() as conn: # get a connection from the pool self.logger.debug('DB connection opened from pool') @@ -337,7 +337,7 @@ async def botperms_add(self, permission:classes.auth.Permission): await conn.commit() except psycopg_pool.PoolTimeout: self.connection_self.connection_pool.check() - async def botperms_get_by_kind(self, kind:str): + async def botperms_get_by_kind(self, kind:str) -> classes.auth.Permission: try: async with self.connection_pool.connection() as conn: # get a connection from the pool self.logger.debug('DB connection opened from pool') @@ -352,7 +352,7 @@ async def botperms_get_by_kind(self, kind:str): return permission_object except psycopg_pool.PoolTimeout: self.connection_self.connection_pool.check() - async def channelref_add(self, channel:classes.auth.Channel): + async def channelref_add(self, channel:classes.auth.Channel) -> None: try: async with self.connection_pool.connection() as conn: # get a connection from the pool self.logger.debug('DB connection opened from pool') @@ -366,7 +366,7 @@ async def channelref_add(self, channel:classes.auth.Channel): await conn.commit() except psycopg_pool.PoolTimeout: self.connection_self.connection_pool.check() - async def channelref_get_by_kind(self, kind:str): + async def channelref_get_by_kind(self, kind:str) -> classes.auth.Channel: try: async with self.connection_pool.connection() as conn: # get a connection from the pool self.logger.debug('DB connection opened from pool') From 64c86069815bf3d5aad03aa2a09a3573bb6edd3a Mon Sep 17 00:00:00 2001 From: HippoProgrammer <172101796+HippoProgrammer@users.noreply.github.com> Date: Wed, 10 Jun 2026 08:15:41 +0000 Subject: [PATCH 3/4] Type hinting added for db internal lists --- src/customio/db.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/customio/db.py b/src/customio/db.py index 4c25c0a..5bff1bf 100644 --- a/src/customio/db.py +++ b/src/customio/db.py @@ -88,7 +88,7 @@ async def listen_for_new_sse_events(self,callback:function) -> None: self.connection_self.connection_pool.check() except asyncio.CancelledError: self.logger.debug('Listener cancelled') - async def get_by_event(self, event:int): + async def get_by_event(self, event:int) -> classes.sse.Event: try: async with self.connection_pool.connection() as conn: # get a connection from the pool self.logger.debug('DB connection opened from pool') @@ -151,7 +151,7 @@ async def nsqueue_get_by_id(self, id:str) -> classes.wa.Proposal: return proposal except psycopg_pool.PoolTimeout: self.connection_self.connection_pool.check() - async def nsqueue_get_all_legal_by_council_limited(self, council:int = 1, limit:int = 7) -> list: + async def nsqueue_get_all_legal_by_council_limited(self, council:int = 1, limit:int = 7) -> list[classes.wa.Proposal]: """Get all proposals that are legal from the NSQueue, up to the specified limit""" try: async with self.connection_pool.connection() as conn: # get a connection from the pool @@ -233,7 +233,7 @@ async def ifvqueue_get_by_id(self, id:str) -> classes.ifv.IFV: return ifv except psycopg_pool.PoolTimeout: self.connection_self.connection_pool.check() - async def ifvqueue_get_by_author(self, author:int) -> list: + async def ifvqueue_get_by_author(self, author:int) -> list[classes.ifv.IFV]: """Get all IFVs from the IFVQueue with the specified author""" try: async with self.connection_pool.connection() as conn: # get a connection from the pool @@ -254,7 +254,7 @@ async def ifvqueue_get_by_author(self, author:int) -> list: return ifvs except psycopg_pool.PoolTimeout: self.connection_self.connection_pool.check() - async def ifvqueue_get_unauthored_limited(self,limit:int = 7) -> list: + async def ifvqueue_get_unauthored_limited(self,limit:int = 7) -> list[classes.ifv.IFV]: """Get all IFVs from the IFVQueue with no author""" try: async with self.connection_pool.connection() as conn: # get a connection from the pool From e474dbcd37d662c741a08547fd543171dc8ca2a4 Mon Sep 17 00:00:00 2001 From: HippoProgrammer <172101796+HippoProgrammer@users.noreply.github.com> Date: Wed, 10 Jun 2026 08:25:18 +0000 Subject: [PATCH 4/4] Type hinting added to ns, env and main --- src/__main__.py | 25 ++++++++++--------------- src/customio/env.py | 6 +++--- src/customio/ns.py | 20 ++++++++++---------- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/src/__main__.py b/src/__main__.py index 0ec5b6f..4bcaf9f 100644 --- a/src/__main__.py +++ b/src/__main__.py @@ -59,7 +59,7 @@ # define class for IFV modals class IFVSelectionModal(discord.ui.DesignerModal): - def __init__(self, user_id:int, action:str, options_data:list, *args, **kwargs) -> None: + def __init__(self, user_id:int, action:str, options_data:list[classes.ifv.IFV], *args, **kwargs) -> None: """Create an IFVSelectionModal object, for selecting and modifying IFV details.""" super().__init__(*args, **kwargs) # pass args and kwargs to base class logger.debug('Args and kwargs passed to base Modal') @@ -102,7 +102,7 @@ def __init__(self, user_id:int, action:str, options_data:list, *args, **kwargs) logger.debug('Modal initialized') - async def callback(self, interaction): + async def callback(self, interaction:discord.Interaction) -> None: """Callback for an IFVSelectionModal.""" success = discord.Embed(description = "IFV modified successfully!").set_footer(text = 'The queue embed may take 1-5 seconds to refresh.') failure_invalid_url = discord.Embed(description = "IFV was not modified.").set_footer(text = 'Please provide a link that is a valid URL.') @@ -143,12 +143,12 @@ async def callback(self, interaction): # define class for IFV view class IFVView(discord.ui.View): - def __init__(self, council:int, *args, **kwargs): + def __init__(self, council:int, *args, **kwargs) -> None: super().__init__(*args, **kwargs) # pass args and kwargs to base class logger.debug('Args and kwargs passed to base Modal') self.council = council - async def _button(self, button, interaction): + async def _button(self, button:discord.ui.Button, interaction:discord.Interaction) -> None: """Private method to handle button callbacks.""" action = button.custom_id # redefine some basic information user_id = interaction.user.id @@ -170,15 +170,15 @@ async def _button(self, button, interaction): logger.info('Modal submitted, embed refreshed') @discord.ui.button(label="Accept IFV", style=discord.ButtonStyle.success, custom_id='accept') # accept IFV button - async def accept(self, button:discord.ui.Button, interaction:discord.Interaction): # pass onto _button handler + async def accept(self, button:discord.ui.Button, interaction:discord.Interaction) -> None: # pass onto _button handler await self._button(button, interaction) @discord.ui.button(label="Submit IFV", style=discord.ButtonStyle.primary, custom_id='submit') # submit IFV button - async def submit(self, button:discord.ui.Button, interaction:discord.Interaction): # pass onto _button handler + async def submit(self, button:discord.ui.Button, interaction:discord.Interaction) -> None: # pass onto _button handler await self._button(button, interaction) @discord.ui.button(label="Remove IFV", style=discord.ButtonStyle.danger, custom_id='remove') # remove IFV button - async def remove(self, button:discord.ui.Button, interaction:discord.Interaction): # pass onto _button handler + async def remove(self, button:discord.ui.Button, interaction:discord.Interaction) -> None: # pass onto _button handler await self._button(button, interaction) @@ -241,7 +241,7 @@ async def _fetch_proposals() -> None: logger.info('Proposal thread being created') logger.info('Proposal data parsed and stored') -async def _new_sse_event(payload:str): +async def _new_sse_event(payload:str) -> None: logger.debug('New SSE event received, checking proposals...') await _fetch_proposals() @@ -322,11 +322,6 @@ async def _get_queue_embed(council:int) -> discord.Embed: emoji = '🔴' else: emoji = '🟢' - - '''if len(reactions) == 2: # this was needed once... but then a bug magically resolved itself. leaving here just in case. - emoji = '🟢/🔴' - else: - emoji = reactions[0].emoji''' logger.debug('Proposal information formatted') @@ -345,7 +340,7 @@ async def _get_queue_embed(council:int) -> discord.Embed: logger.info('Embed object created') return embed # return it -async def _show_queue(ctx: discord.ApplicationContext, council:int): +async def _show_queue(ctx: discord.ApplicationContext, council:int) -> None: if await _check_perms(ctx, 'user'): await ctx.defer(ephemeral = True) logger.info('Fetching queue embed') @@ -361,7 +356,7 @@ async def _show_queue(ctx: discord.ApplicationContext, council:int): await ctx.respond(embed = embed, ephemeral = True) logger.info('Error embed sent') -async def _announce_queue(ctx: discord.ApplicationContext, council:int, ping_users:bool): +async def _announce_queue(ctx: discord.ApplicationContext, council:int, ping_users:bool) -> None: if await _check_perms(ctx, 'admin'): await ctx.defer() # the deferral must be here so the 'No Permissions' embed can be sent ephemerally logger.info('Fetching queue embed') diff --git a/src/customio/env.py b/src/customio/env.py index cfeab0e..222cbac 100644 --- a/src/customio/env.py +++ b/src/customio/env.py @@ -20,7 +20,7 @@ # set up a logger logger = logging.getLogger('assembly.customio.env') # get the logger for this script -def load_secrets_from_envvars(): +def load_secrets_from_envvars() -> tuple[str, str]: # load envvars token_file = str(os.getenv("ASSEMBLY_TOKEN_FILE")) pgpass_file = str(os.getenv("POSTGRES_PASSWORD_FILE")) @@ -44,7 +44,7 @@ def load_secrets_from_envvars(): pgpass = file.read() return token, pgpass -def load_database_config_from_envvars(): +def load_database_config_from_envvars() -> tuple[str, str, str, str, str]: user = str(os.getenv("POSTGRES_USER")) host = str(os.getenv("POSTGRES_HOST")) port = str(os.getenv("POSTGRES_PORT")) @@ -52,6 +52,6 @@ def load_database_config_from_envvars(): akari_db = str(os.getenv("POSTGRES_AKARI_DB")) return user, host, port, assembly_db, akari_db -def load_useragent_from_envvars(): +def load_useragent_from_envvars() -> str: useragent_nation = str(os.getenv("NS_USER_AGENT")) return useragent_nation \ No newline at end of file diff --git a/src/customio/ns.py b/src/customio/ns.py index 31bf61e..340fd2b 100644 --- a/src/customio/ns.py +++ b/src/customio/ns.py @@ -30,19 +30,19 @@ class HTTPResponseException(Exception): class QueryException(Exception): pass class API: - def __init__(self): + def __init__(self) -> None: self.rate_limited = False self.headers = { "User-Agent": f"assembly/0.1.0-a1, source https://github.com/HippoProgrammer/assembly, author idinist_imauggland, used_by {load_useragent_from_envvars()}" } - async def setup_all(self): + async def setup_all(self) -> None: self.clientsession = aiohttp.ClientSession(headers=self.headers) async def cleanup(self) -> None: await self.clientsession.close() - async def _make_request(self, uri:str): + async def _make_request(self, uri:str) -> str: if not self.rate_limited: async with self.clientsession.get(uri) as response: if response.status == 200: @@ -58,7 +58,7 @@ async def _make_request(self, uri:str): else: raise QueryException('Rate limited. Request blocked.') - async def _query_proposals(self, council: int): + async def _query_proposals(self, council: int) -> etree.ElementTree: council = str(council) # convert to string for URL try: xmlstr = await self._make_request(f'http://www.nationstates.net/cgi-bin/api.cgi?wa={council}&q=proposals') @@ -68,13 +68,13 @@ async def _query_proposals(self, council: int): except HTTPResponseException as e: raise QueryException(str(e)) - async def _parse_coauthor(self,coauthor:etree._Element): + async def _parse_coauthor(self,coauthor:etree.Element) -> list[str | None]: if len(coauthor) == 0: return [] else: return coauthor[0].text.split(',') - async def _get_quorum(self): + async def _get_quorum(self) -> int: try: xmlstr = await self._make_request('http://www.nationstates.net/cgi-bin/api.cgi?wa=1&q=numdelegates') xmltree = etree.fromstring(xmlstr) @@ -84,13 +84,13 @@ async def _get_quorum(self): except HTTPResponseException as e: raise QueryException(str(e)) - async def _parse_approvals(self,approval:etree._Element): + async def _parse_approvals(self,approval:etree._Element) -> list[str | None]: if approval[0].text == None: return [] else: return approval[0].text.split(':') - async def parse_proposals(self, council: int): + async def parse_proposals(self, council: int) -> list[classes.wa.Proposal]: xml = await self._query_proposals(council) parsed_xml = [] for element in xml: @@ -107,7 +107,7 @@ async def parse_proposals(self, council: int): parsed_xml.append(parsed_element) return parsed_xml - async def _query_atvote(self,council:int): + async def _query_atvote(self,council:int) -> etree.ElementTree: council = str(council) # convert to string for URL try: xmlstr = await self._make_request(f'http://www.nationstates.net/cgi-bin/api.cgi?wa={council}&q=resolution') @@ -118,4 +118,4 @@ async def _query_atvote(self,council:int): else: return resolutions except HTTPResponseException as e: - raise QueryException(str(e)) + raise QueryException(str(e))