diff --git a/.gitignore b/.gitignore index 8d40064..98b7c0f 100644 --- a/.gitignore +++ b/.gitignore @@ -163,4 +163,6 @@ pyrightconfig.json # direnv files, used by load python venv .direnv/ -.envrc \ No newline at end of file +.envrc +.local.env +.envrc diff --git a/README.md b/README.md index 944fd70..ecf5ee6 100644 --- a/README.md +++ b/README.md @@ -40,3 +40,7 @@ A `Makefile` is provided with the following targets: - `htmlcov` : run the unit tests and generate a full report in htmlcov/ Testing and coverage requires standing up a local testbed. For details, see [Design](docs/design.md). + + +## Adding `llm-redactor` branch +For the redactor feature. \ No newline at end of file diff --git a/compose.yaml b/compose.yaml index 788d64a..a3f8cdd 100644 --- a/compose.yaml +++ b/compose.yaml @@ -7,5 +7,7 @@ services: - DISCORD_TOKEN=${DISCORD_TOKEN} - REDMINE_TOKEN=${REDMINE_TOKEN} - REDMINE_URL=${REDMINE_URL} + volumes: + - ./redaction_queue.json:/app/redaction_queue.json #share queue file restart: on-failure:1 - network_mode: host + network_mode: host \ No newline at end of file diff --git a/data/custom_fields.json b/data/custom_fields.json index 127173f..26b8b41 100644 --- a/data/custom_fields.json +++ b/data/custom_fields.json @@ -1 +1,207 @@ -{"custom_fields":[{"id":2,"name":"Discord ID","description":"ID used to link user to their discord account, to enable integration","customized_type":"user","field_format":"string","regexp":"","min_length":null,"max_length":null,"is_required":false,"is_filter":false,"searchable":false,"multiple":false,"default_value":"","visible":true},{"id":4,"name":"syncdata","description":"Metadata used to sync the ticket with external sources.\r\n\r\nCurrent format is thread-id|zulu-timestamp.\r\n\r\nNot recommend to edit.","customized_type":"issue","field_format":"string","regexp":"","min_length":null,"max_length":null,"is_required":false,"is_filter":true,"searchable":false,"multiple":false,"default_value":"","visible":false,"trackers":[{"id":2,"name":"Infra-Field"},{"id":4,"name":"Software-Dev"},{"id":6,"name":"Infra-Config"},{"id":8,"name":"External-Comms-Intake"},{"id":9,"name":"Outreach-Partnerships"},{"id":10,"name":"Admin"},{"id":17,"name":"Research"},{"id":18,"name":"Mutual-Aid-Action"},{"id":19,"name":"SCN-Space"}],"roles":[{"id":3,"name":"Administrator"}]},{"id":5,"name":"To/CC","description":"Contains the To and Cc headers from the email that created the ticket.","customized_type":"issue","field_format":"string","regexp":"","min_length":null,"max_length":null,"is_required":false,"is_filter":false,"searchable":true,"multiple":false,"default_value":"","visible":true,"trackers":[{"id":2,"name":"Infra-Field"},{"id":4,"name":"Software-Dev"},{"id":6,"name":"Infra-Config"},{"id":8,"name":"External-Comms-Intake"},{"id":9,"name":"Outreach-Partnerships"},{"id":10,"name":"Admin"},{"id":13,"name":"Test-Reject"},{"id":17,"name":"Research"},{"id":18,"name":"Mutual-Aid-Action"},{"id":19,"name":"SCN-Space"}],"roles":[]}]} +{ + "custom_fields": [ + { + "id": 2, + "name": "Discord ID", + "description": "ID used to link user to their discord account, to enable integration", + "customized_type": "user", + "field_format": "string", + "regexp": "", + "min_length": null, + "max_length": null, + "is_required": false, + "is_filter": false, + "searchable": false, + "multiple": false, + "default_value": "", + "visible": true, + "editable": true + }, + { + "id": 4, + "name": "syncdata", + "description": "Metadata used to sync the ticket with external sources.\r\n\r\nCurrent format is thread-id|zulu-timestamp.\r\n\r\nNot recommend to edit.", + "customized_type": "issue", + "field_format": "string", + "regexp": "", + "min_length": null, + "max_length": null, + "is_required": false, + "is_filter": true, + "searchable": false, + "multiple": false, + "default_value": "", + "visible": false, + "editable": true, + "trackers": [ + { + "id": 2, + "name": "Infra-Field" + }, + { + "id": 4, + "name": "Software-Dev" + }, + { + "id": 6, + "name": "Infra-Config" + }, + { + "id": 8, + "name": "External-Comms-Intake" + }, + { + "id": 9, + "name": "Outreach-Partnerships" + }, + { + "id": 10, + "name": "Admin" + }, + { + "id": 17, + "name": "Research" + }, + { + "id": 18, + "name": "Mutual-Aid-Action" + }, + { + "id": 19, + "name": "SCN-Space" + } + ], + "roles": [ + { + "id": 3, + "name": "Administrator" + } + ] + }, + { + "id": 5, + "name": "To/CC", + "description": "Contains the To and Cc headers from the email that created the ticket.", + "customized_type": "issue", + "field_format": "string", + "regexp": "", + "min_length": null, + "max_length": null, + "is_required": false, + "is_filter": false, + "searchable": true, + "multiple": false, + "default_value": "", + "visible": true, + "editable": true, + "trackers": [ + { + "id": 2, + "name": "Infra-Field" + }, + { + "id": 4, + "name": "Software-Dev" + }, + { + "id": 6, + "name": "Infra-Config" + }, + { + "id": 8, + "name": "External-Comms-Intake" + }, + { + "id": 9, + "name": "Outreach-Partnerships" + }, + { + "id": 10, + "name": "Admin" + }, + { + "id": 13, + "name": "Test-Reject" + }, + { + "id": 17, + "name": "Research" + }, + { + "id": 18, + "name": "Mutual-Aid-Action" + }, + { + "id": 19, + "name": "SCN-Space" + } + ], + "roles": [] + }, + { + "id": 6, + "name": "redacted", + "description": "Keys and values for the redacted fields in the ticket.", + "customized_type": "issue", + "field_format": "string", + "regexp": "", + "min_length": null, + "max_length": null, + "is_required": false, + "is_filter": false, + "searchable": false, + "multiple": false, + "default_value": "", + "visible": false, + "editable": true, + "trackers": [ + { + "id": 2, + "name": "Infra-Field" + }, + { + "id": 4, + "name": "Software-Dev" + }, + { + "id": 6, + "name": "Infra-Config" + }, + { + "id": 8, + "name": "External-Comms-Intake" + }, + { + "id": 9, + "name": "Outreach-Partnerships" + }, + { + "id": 10, + "name": "Admin" + }, + { + "id": 13, + "name": "Test-Reject" + }, + { + "id": 17, + "name": "Research" + }, + { + "id": 18, + "name": "Mutual-Aid-Action" + }, + { + "id": 19, + "name": "SCN-Space" + } + ], + "roles": [ + { + "id": 3, + "name": "Administrator" + } + ] + } + ] +} \ No newline at end of file diff --git a/netbot/cog_scn.py b/netbot/cog_scn.py index 9c3e2a5..c3d675f 100644 --- a/netbot/cog_scn.py +++ b/netbot/cog_scn.py @@ -1,17 +1,16 @@ #!/usr/bin/env python3 """Cog to manage SCN-related functions""" + import logging import discord - -from discord.commands import option, SlashCommandGroup +from discord.commands import SlashCommandGroup, option from discord.ext import commands from discord.utils import basic_autocomplete -from redmine.model import Message, User -from redmine.redmine import Client, BLOCKED_TEAM_NAME - from netbot.netbot import NetBot, default_ticket +from redmine.model import Message, User +from redmine.redmine import BLOCKED_TEAM_NAME, Client log = logging.getLogger(__name__) @@ -24,12 +23,15 @@ # scn reindex + def setup(bot): bot.add_cog(SCNCog(bot)) log.info("initialized SCN cog") + class NewUserModal(discord.ui.Modal): """modal dialog to collect new user info""" + def __init__(self, redmine: Client, login: str, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.redmine = redmine @@ -38,7 +40,6 @@ def __init__(self, redmine: Client, login: str, *args, **kwargs) -> None: self.add_item(discord.ui.InputText(label="Last Name")) self.add_item(discord.ui.InputText(label="Email")) - async def callback(self, interaction: discord.Interaction): email = self.children[2].value first = self.children[0].value @@ -48,12 +49,18 @@ async def callback(self, interaction: discord.Interaction): user = self.redmine.user_mgr.register(self.login, email, first, last) if user is None: - log.error(f"Unable to create user for {self.login}, {first} {last}, {email}") - await interaction.response.send_message(f"Unable to create user for {self.login}") + log.error( + f"Unable to create user for {self.login}, {first} {last}, {email}" + ) + await interaction.response.send_message( + f"Unable to create user for {self.login}" + ) return # create the mapping so it the discord user can be found - self.redmine.user_mgr.create_discord_mapping(user, interaction.user.id, interaction.user.name) + self.redmine.user_mgr.create_discord_mapping( + user, interaction.user.id, interaction.user.name + ) log.debug(f"mapped discord new user: {interaction.user.name} -> {user.login}") embed = discord.Embed(title="Registered User") @@ -65,20 +72,19 @@ async def callback(self, interaction: discord.Interaction): class ApproveButton(discord.ui.Button): """Discord button to approve specific users""" + def __init__(self, bot_: discord.Bot, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.bot = bot_ - # TODO Move to user_mgr? - def find_registered_user(self, discord_name:str) -> User: + def find_registered_user(self, discord_name: str) -> User: """Search registered users for a matching discord ID""" for user in self.bot.redmine.user_mgr.get_registered(): if user.discord_id == discord_name: return user return None - async def callback(self, interaction: discord.Interaction): name = self.label @@ -86,13 +92,16 @@ async def callback(self, interaction: discord.Interaction): if user: self.bot.redmine.user_mgr.approve(user) # assign default groups? - await interaction.response.send_message(f"Approved registered user: @{name} {user.login} {user.name}") + await interaction.response.send_message( + f"Approved registered user: @{name} {user.login} {user.name}" + ) else: await interaction.response.send_message(f"User not found: {name}") class ApproveUserView(discord.ui.View): """Approve registered users with Discord controls""" + def __init__(self, bot_: discord.Bot, users: list[User]) -> None: self.bot = bot_ super().__init__() @@ -101,10 +110,12 @@ def __init__(self, bot_: discord.Bot, users: list[User]) -> None: for user in users: self.add_item(ApproveButton(self.bot, label=user.discord)) - - async def button_callback(self, button: discord.ui.Button, interaction: discord.Interaction): - await interaction.response.send_message(f"ApproveUserView: {button} {interaction}") - + async def button_callback( + self, button: discord.ui.Button, interaction: discord.Interaction + ): + await interaction.response.send_message( + f"ApproveUserView: {button} {interaction}" + ) async def callback(self, interaction: discord.Interaction): await interaction.response.send_message(f"ApproveUserView: {interaction}") @@ -113,6 +124,7 @@ async def callback(self, interaction: discord.Interaction): # FIXME Not yet implemented class IntakeView(discord.ui.View): """Perform intake""" + # to build, need: # - list of trackers # - list or priorities @@ -128,24 +140,30 @@ def __init__(self, bot_: discord.Bot) -> None: super().__init__() # Adds the dropdown to our View object - #self.add_item(PrioritySelect(self.bot)) - #self.add_item(discord.ui.InputText(label="Subject", row=1)) - #self.add_item(TrackerSelect(self.bot)) - + # self.add_item(PrioritySelect(self.bot)) + # self.add_item(discord.ui.InputText(label="Subject", row=1)) + # self.add_item(TrackerSelect(self.bot)) self.add_item(discord.ui.Button(label="Assign", row=4)) self.add_item(discord.ui.Button(label="Reject ticket subject", row=4)) self.add_item(discord.ui.Button(label="Block email@address.com", row=4)) - async def select_callback(self, select, interaction): # the function called when the user is done selecting options - await interaction.response.send_message(f"IntakeView.select_callback() selected: {select.values[0]}") + async def select_callback( + self, select, interaction + ): # the function called when the user is done selecting options + await interaction.response.send_message( + f"IntakeView.select_callback() selected: {select.values[0]}" + ) async def callback(self, interaction: discord.Interaction): - await interaction.response.send_message(f"IntakeView.allback() {interaction.data}") + await interaction.response.send_message( + f"IntakeView.allback() {interaction.data}" + ) class SCNCog(commands.Cog): """Cog to mange SCN-related functions""" + def __init__(self, bot): self.bot = bot self.formatter = bot.formatter @@ -153,84 +171,115 @@ def __init__(self, bot): # see https://github.com/Pycord-Development/pycord/blob/master/examples/app_commands/slash_cog_groups.py - scn = SlashCommandGroup("scn", "SCN admin commands") - + scn = SlashCommandGroup("scn", "SCN admin commands") def is_admin(self, user: discord.Member) -> bool: """Check if the given Discord memeber is in a authorized role""" # search user for "auth" role for role in user.roles: - if "auth" == role.name: ## FIXME + if "auth" == role.name: ## FIXME return True # auth role not found return False - # FIXME rename to "register"? @scn.command(description="Add a Discord user to redmine") - @option("ticket_id", description="ticket ID", autocomplete=basic_autocomplete(default_ticket)) - @option("member", description="Discord member collaborating with ticket", optional=True) - async def add(self, ctx:discord.ApplicationContext, redmine_login:str, member:discord.Member=None): + @option( + "ticket_id", + description="ticket ID", + autocomplete=basic_autocomplete(default_ticket), + ) + @option( + "member", description="Discord member collaborating with ticket", optional=True + ) + async def add( + self, + ctx: discord.ApplicationContext, + redmine_login: str, + member: discord.Member = None, + ): """add a Discord user to the Redmine ticketing integration""" - discord_id = ctx.user # by default, assume current user + discord_id = ctx.user # by default, assume current user if member: - log.info(f"Overriding current user={ctx.user.name} with member={member.name}") + log.info( + f"Overriding current user={ctx.user.name} with member={member.name}" + ) discord_id = member user = self.redmine.user_mgr.find(discord_id.name) if user: # check the id_from_user = user.discord_id - if id_from_user.id > 0: + if id_from_user and id_from_user.id > 0: # a valid - await ctx.respond(f"Discord user: {discord_id} is fully configured as redmine user: {user.login}") + await ctx.respond( + f"Discord user: {discord_id} is fully configured as redmine user: {user.login}" + ) else: # need to update - self.redmine.user_mgr.create_discord_mapping(user, discord_id.id, discord_id.name) - await ctx.respond(f"Discord user: {discord_id.id},{discord_id.name} has been paired with redmine user: {redmine_login}") + self.redmine.user_mgr.create_discord_mapping( + user, discord_id.id, discord_id.name + ) + await ctx.respond( + f"Discord user: {discord_id.id},{discord_id.name} has been paired with redmine user: {redmine_login}" + ) else: user = self.redmine.user_mgr.find(redmine_login) if user and self.is_admin(ctx.user): - self.redmine.user_mgr.create_discord_mapping(user, discord_id.id, discord_id.name) - await ctx.respond(f"Discord user: {discord_id.name} has been paired with redmine user: {redmine_login}") + self.redmine.user_mgr.create_discord_mapping( + user, discord_id.id, discord_id.name + ) + await ctx.respond( + f"Discord user: {discord_id.name} has been paired with redmine user: {redmine_login}" + ) else: # case: unknown redmine_login -> new user request: register new user - modal = NewUserModal(self.redmine, redmine_login, title="Register new user") + modal = NewUserModal( + self.redmine, redmine_login, title="Register new user" + ) await ctx.send_modal(modal) # reindex users after changes self.redmine.user_mgr.reindex_users() - @scn.command() - async def sync(self, ctx:discord.ApplicationContext): + async def sync(self, ctx: discord.ApplicationContext): """syncronize an existing ticket thread with redmine""" if isinstance(ctx.channel, discord.Thread): thread = ctx.channel ticket = await self.bot.sync_thread(thread) if ticket: - await ctx.respond(f"SYNC ticket {ticket.id} to thread: {thread.name} complete") + await ctx.respond( + f"SYNC ticket {ticket.id} to thread: {thread.name} complete" + ) else: # double-check thread name ticket_id = NetBot.parse_thread_title(thread.name) if ticket_id: - await ctx.respond(f"No ticket (#{ticket_id}) found for thread named: {thread.name}") + await ctx.respond( + f"No ticket (#{ticket_id}) found for thread named: {thread.name}" + ) else: # create new ticket subject = thread.name user = self.redmine.user_mgr.find(ctx.user.name) - message = Message(user.login, subject) # user.mail? - message.note = subject + "\n\nCreated by netbot by syncing Discord thread with same name." + message = Message(user.login, subject) # user.mail? + message.note = ( + subject + + "\n\nCreated by netbot by syncing Discord thread with same name." + ) ticket = self.redmine.ticket_mgr.create(user, message) # set tracker # TODO: search up all parents in hierarchy? - tracker = self.bot.redmine.ticket_mgr.get_tracker(thread.parent.name) + tracker = self.bot.redmine.ticket_mgr.get_tracker( + thread.parent.name + ) if tracker: log.debug(f"found {thread.parent.name} => {tracker}") params = { "tracker_id": str(tracker.id), - "notes": f"Setting tracker based on channel name: {thread.parent.name}" + "notes": f"Setting tracker based on channel name: {thread.parent.name}", } self.redmine.ticket_mgr.update(ticket.id, params, user.login) else: @@ -240,22 +289,20 @@ async def sync(self, ctx:discord.ApplicationContext): await thread.edit(name=f"Ticket #{ticket.id}: {ticket.subject}") # sync the thread - ticket = await self.bot.sync_thread(thread) # refesh the ticket + ticket = await self.bot.sync_thread(thread) # refesh the ticket await ctx.respond(self.bot.formatter.format_ticket(ticket)) - #OLD await ctx.respond(f"Cannot find ticket# in thread name: {ctx.channel.name}") # error + # OLD await ctx.respond(f"Cannot find ticket# in thread name: {ctx.channel.name}") # error else: - await ctx.respond("Not a thread.") # error - + await ctx.respond("Not a thread.") # error @scn.command() - async def reindex(self, ctx:discord.ApplicationContext): + async def reindex(self, ctx: discord.ApplicationContext): """reindex the all cached information""" self.redmine.reindex() - #self.bot.reindex() FIXME, once roles are working + # self.bot.reindex() FIXME, once roles are working await ctx.respond("Rebuilt redmine indices.") - # REMOVE - handled by discord roles # @scn.command(description="join the specified team") # async def join(self, ctx:discord.ApplicationContext, teamname:str , member: discord.Member=None): @@ -288,15 +335,15 @@ async def reindex(self, ctx:discord.ApplicationContext): # else: # await ctx.respond(f"Unknown Discord user: {discord_name}.") - - def find_role(self, ctx:discord.ApplicationContext, rolename:str) -> discord.Role | None: + def find_role( + self, ctx: discord.ApplicationContext, rolename: str + ) -> discord.Role | None: for role in ctx.guild.roles: if role.name == rolename: return role - @scn.command(description="list teams and members") - async def teams(self, ctx:discord.ApplicationContext, teamname:str=None): + async def teams(self, ctx: discord.ApplicationContext, teamname: str = None): # list teams, with members if teamname: team = self.find_role(ctx, teamname) @@ -304,8 +351,16 @@ async def teams(self, ctx:discord.ApplicationContext, teamname:str=None): await ctx.respond(self.formatter.format_team(ctx, team)) return else: - all_teams = ", ".join([team.name for team in ctx.guild.roles if team.name.endswith("-team")]) - await ctx.respond(f"Unknown team name: {teamname}\nTeams: {all_teams}") # error + all_teams = ", ".join( + [ + team.name + for team in ctx.guild.roles + if team.name.endswith("-team") + ] + ) + await ctx.respond( + f"Unknown team name: {teamname}\nTeams: {all_teams}" + ) # error else: # all teams buff = "" @@ -313,38 +368,38 @@ async def teams(self, ctx:discord.ApplicationContext, teamname:str=None): buff += self.formatter.format_team(ctx, team, inc_users=False) await ctx.respond(buff) - - #@scn.command() - #async def intake(self, ctx:discord.ApplicationContext): + # @scn.command() + # async def intake(self, ctx:discord.ApplicationContext): # """perform intake""" # # check team? admin?, provide reasonable error msg. # await ctx.respond("INTAKE #{ticket.id}", view=IntakeView(self.bot)) - @scn.command(description="list all open epics") - async def epics(self, ctx:discord.ApplicationContext): + async def epics(self, ctx: discord.ApplicationContext): """List all the epics, grouped by tracker""" # get the epics. epics = self.redmine.ticket_mgr.get_epics() # format the epics and respond await ctx.respond(embeds=self.bot.formatter.epics_embed(ctx, epics)) - @scn.command(description="list blocked email") - async def blocked(self, ctx:discord.ApplicationContext): + async def blocked(self, ctx: discord.ApplicationContext): team = self.redmine.user_mgr.cache.get_team_by_name(BLOCKED_TEAM_NAME) if team: await ctx.respond(self.formatter.format_team(team)) else: - await ctx.respond(f"Expected team {BLOCKED_TEAM_NAME} not configured") # error - + await ctx.respond( + f"Expected team {BLOCKED_TEAM_NAME} not configured" + ) # error # ticket 484 - http://10.10.0.218/issues/484 # block users based on name (not discord membership) - @scn.command(description="block specific a email address and reject all related tickets") - async def block(self, ctx:discord.ApplicationContext, username:str): + @scn.command( + description="block specific a email address and reject all related tickets" + ) + async def block(self, ctx: discord.ApplicationContext, username: str): log.debug(f"blocking {username}") - #user = self.redmine.lookup_user(username) + # user = self.redmine.lookup_user(username) user = self.redmine.user_mgr.find(username) if user: # add the user to the blocked list @@ -352,14 +407,15 @@ async def block(self, ctx:discord.ApplicationContext, username:str): # search and reject all tickets from that user for ticket in self.redmine.ticket_mgr.get_by(user): self.redmine.ticket_mgr.reject_ticket(ticket.id) - await ctx.respond(f"Blocked user: {user.login} and rejected all created tickets") + await ctx.respond( + f"Blocked user: {user.login} and rejected all created tickets" + ) else: log.debug("trying to block unknown user '{username}', ignoring") await ctx.respond(f"Unknown user: {username}") - @scn.command(description="unblock specific a email address") - async def unblock(self, ctx:discord.ApplicationContext, username:str): + async def unblock(self, ctx: discord.ApplicationContext, username: str): log.debug(f"Unblocking {username}") user = self.redmine.user_mgr.find(username) if user: @@ -369,28 +425,60 @@ async def unblock(self, ctx:discord.ApplicationContext, username:str): log.debug("trying to unblock unknown user '{username}', ignoring") await ctx.respond(f"Unknown user: {username}") - @scn.command(name="force-notify", description="Force ticket notifications") - async def force_remind(self, ctx:discord.ApplicationContext): + async def force_remind(self, ctx: discord.ApplicationContext): await ctx.respond("Sending reminders for dusty tickets....") await self.bot.remind_dusty_tickets() - @scn.command(name="force-recycle", description="Force ticket notifications") - async def force_recycle(self, ctx:discord.ApplicationContext): + async def force_recycle(self, ctx: discord.ApplicationContext): await ctx.respond("Recycling dusty, old tickets....") await self.bot.recycle_tickets() - @scn.command(description="List and approve registered new users") - async def approve(self, ctx:discord.ApplicationContext): + async def approve(self, ctx: discord.ApplicationContext): if self.is_admin(ctx.user): # get the registered users users = self.bot.redmine.user_mgr.get_registered() if len(users) > 0: - await ctx.respond("Approve Registered Users", view=ApproveUserView(self.bot, users)) + await ctx.respond( + "Approve Registered Users", view=ApproveUserView(self.bot, users) + ) else: await ctx.respond("No pending registered users.") else: await ctx.respond("Must be authorized admin to approve Redmine users.") + + # -------- + + # @scn.command(description="Redact a ticket") + # async def redact(self, ctx:discord.ApplicationContext, ticket_id: int): + # if self.is_admin(ctx.user): + # ticket = self.redmine.ticket_mgr.get(ticket_id) + # if ticket: + # ticket = self.redmine.redact_ticket(ticket) + # await self.bot.formatter.print_ticket(ticket, ctx) + # else: + # # TODO user error + # await ctx.respond(f"Ticket {ticket_id} not found.", ephemeral=True) + + # @scn.command(description="Display ticket details with redacted info displayed") + # async def unredact(self, ctx:discord.ApplicationContext, ticket_id: int): + # if self.bot.is_pii_admin(ctx.user): #TODO CHANGE LATER + # await ctx.defer(ephemeral=True) # TODO CHANGE LATER + # ticket = self.redmine.ticket_mgr.get(ticket_id) + # if ticket: + # # ticket = self.redmine.unredact_ticket(ticket) TODO CHANGE LATER + # #await self.bot.formatter.print_ticket(ticket, ctx) + + # # await ctx.respond(embed=self.formatter.ticket_embed(ctx, ticket), ephemeral=True) + # await ctx.followup.send(embed=self.formatter.ticket_embed(ctx, ticket), ephemeral=True) # Changed to followup TODO CHANGE LATER + + # else: + # # TODO user error + # # await ctx.respond(f"Ticket {ticket_id} not found.") + # await ctx.followup.send(f"Ticket {ticket_id} not found.") # Changed to followup TODO CHANGE LATER + # else + # ticket = self.redmine.ticket_mgr.get(ticket_id) + # await ctx.followup.send(embed=self.formatter.ticket_embed(ctx, ticket), ephemeral=True) diff --git a/netbot/cog_tickets.py b/netbot/cog_tickets.py index bc23583..dfeba74 100644 --- a/netbot/cog_tickets.py +++ b/netbot/cog_tickets.py @@ -253,34 +253,83 @@ async def callback(self, interaction: discord.Interaction): await interaction.response.send_message(embeds=[embed]) +# class EditDescriptionModal(discord.ui.Modal): +# """modal dialog to edit the ticket subject and description""" +# def __init__(self, redmine: Client, ticket: Ticket, *args, **kwargs) -> None: +# super().__init__(*args, **kwargs) +# # Note: redmine must be available in callback, as the bot is not +# # available thru the Interaction. +# self.redmine = redmine +# self.ticket_id = ticket.id +# self.add_item(discord.ui.InputText(label="Description", +# value=ticket.description, +# style=InputTextStyle.paragraph)) + + +# async def callback(self, interaction: discord.Interaction): +# description = self.children[0].value +# log.debug(f"callback: {description}") + +# user = self.redmine.user_mgr.find_discord_user(interaction.user.name) + +# fields = { +# "description": description, +# } +# ticket = self.redmine.ticket_mgr.update(self.ticket_id, fields, user.login) + +# embed = discord.Embed(title=f"Updated ticket {ticket.id} description") +# embed.add_field(name="Description", value=ticket.description) + +# await interaction.response.send_message(embeds=[embed]) + class EditDescriptionModal(discord.ui.Modal): """modal dialog to edit the ticket subject and description""" - def __init__(self, redmine: Client, ticket: Ticket, *args, **kwargs) -> None: + def __init__(self, redmine: Client, ticket: Ticket, bot: NetBot, *args, **kwargs) -> None: super().__init__(*args, **kwargs) - # Note: redmine must be available in callback, as the bot is not - # available thru the Interaction. self.redmine = redmine self.ticket_id = ticket.id - self.add_item(discord.ui.InputText(label="Description", - value=ticket.description, - style=InputTextStyle.paragraph)) - + self.bot = bot + self.add_item(discord.ui.InputText( + label="Description", + value=ticket.get_custom_field("unredacted") or ticket.description, + style=InputTextStyle.paragraph + )) async def callback(self, interaction: discord.Interaction): + from redaction_queue import RedactionQueue + description = self.children[0].value - log.debug(f"callback: {description}") - + log.debug(f"Edit callback for ticket #{self.ticket_id}") + + queue = RedactionQueue() + + # Check if ticket is locked + # if queue.is_locked(self.ticket_id): + # await interaction.response.send_message( + # "Can't edit right now. Please wait and try again in a few minutes.", + # ephemeral=True + # ) + # return + + # Get user info user = self.redmine.user_mgr.find_discord_user(interaction.user.name) - - fields = { - "description": description, + user_info = { + "name": user.name if user else interaction.user.name, + "login": user.login if user else None, + "discord_id": interaction.user.id } - ticket = self.redmine.ticket_mgr.update(self.ticket_id, fields, user.login) - - embed = discord.Embed(title=f"Updated ticket {ticket.id} description") - embed.add_field(name="Description", value=ticket.description) - - await interaction.response.send_message(embeds=[embed]) + + # Add to queue + job_id = queue.add_edit_job(self.ticket_id, description, user_info) + + await interaction.response.send_message( + f"Your edit has been queued for redaction.\n" + f"This will take approximately 15-20 minutes.\n\n" + f"Ticket #{self.ticket_id} is locked during this process.", + ephemeral=True + ) + + log.info(f"Queued edit job {job_id} for ticket #{self.ticket_id}") # distinct from above. takes app-context @@ -302,6 +351,10 @@ def __init__(self, bot:NetBot): self.bot:NetBot = bot self.redmine: Client = bot.redmine + def is_pii_admin(self, user: discord.Member) -> bool: + """Check if user has PII admin role""" + return self.bot.is_pii_admin(user) + # see https://github.com/Pycord-Development/pycord/blob/master/examples/app_commands/slash_cog_groups.py ticket = SlashCommandGroup("ticket", "ticket commands") @@ -382,18 +435,28 @@ async def query(self, ctx: discord.ApplicationContext, term:str = ""): else: await ctx.respond(f"Zero results for: `{term}`") - - @ticket.command(description="Get ticket details") @option("ticket_id", description="ticket ID", autocomplete=basic_autocomplete(default_ticket)) - async def details(self, ctx: discord.ApplicationContext, ticket_id:int): - """Update status on a ticket, using: unassign, resolve, progress""" - #log.debug(f"found user mapping for {ctx.user.name}: {user}") + async def details(self, ctx: discord.ApplicationContext, ticket_id: int): + #Show ticket details always ephemeral + #PII admin: pulls unredacted CF and swaps into description for display + #Regular user: shows description as is (already redacted) ticket = self.redmine.ticket_mgr.get(ticket_id, include="children,watchers") - if ticket: - await self.bot.formatter.print_ticket(ticket, ctx) - else: - await ctx.respond(f"Ticket {ticket_id} not found.") # print error + + if not ticket: + await ctx.respond(f"Ticket {ticket_id} not found.", ephemeral=True) + return + + if self.is_pii_admin(ctx.user): + #pull original PII from unredacted CF and swap into description for UI DISPLAY ONLY + unredacted_value = ticket.get_custom_field("unredacted") + if unredacted_value: + ticket.description = unredacted_value + + await ctx.respond( + embed=self.bot.formatter.ticket_embed(ctx, ticket), + ephemeral=True + ) @ticket.command(description="Collaborate on a ticket") @@ -523,6 +586,7 @@ async def assign(self, ctx: discord.ApplicationContext, ticket_id:int, member:di async def create_thread(self, ticket:Ticket, ctx:discord.ApplicationContext): log.info(f"creating a new thread for ticket #{ticket.id} in channel: {ctx.channel.name}") thread_name = f"Ticket #{ticket.id}: {ticket.subject}" + if isinstance(ctx.channel, discord.Thread): log.debug(f"creating thread in parent channel {ctx.channel.parent.name}, for {ticket}") thread = await ctx.channel.parent.create_thread(name=thread_name, type=discord.ChannelType.public_thread) @@ -533,7 +597,6 @@ async def create_thread(self, ticket:Ticket, ctx:discord.ApplicationContext): await thread.send(self.bot.formatter.format_ticket_details(ticket)) return thread - @ticket.command(name="new", description="Create a new ticket") @option("title", description="Title of the new SCN ticket") async def create_new_ticket(self, ctx: discord.ApplicationContext, title:str): @@ -806,16 +869,56 @@ async def due(self, ctx: discord.ApplicationContext, date:str): await ctx.respond("Command only valid in ticket thread. No ticket info found in this thread.") + # @ticket.command(name="description", description="Edit the description of a ticket") + # async def edit_description(self, ctx: discord.ApplicationContext): + # # pop the the edit description embed + # ticket_id = NetBot.parse_thread_title(ctx.channel.name) + # ticket = self.redmine.ticket_mgr.get(ticket_id) + # if ticket: + # modal = EditDescriptionModal(self.redmine, ticket, title=f"Editing ticket #{ticket.id}") + # await ctx.send_modal(modal) + # else: + # await ctx.respond(f"Cannot find ticket for {ctx.channel}") + @ticket.command(name="description", description="Edit the description of a ticket") async def edit_description(self, ctx: discord.ApplicationContext): - # pop the the edit description embed + from redaction_queue import RedactionQueue + + # Check PII admin permission + if not self.is_pii_admin(ctx.user): + await ctx.respond( + "You don't have permission to edit ticket descriptions.", + ephemeral=True + ) + return + + # Get ticket from thread name ticket_id = NetBot.parse_thread_title(ctx.channel.name) + if not ticket_id: + await ctx.respond( + "This command only works in ticket threads.", + ephemeral=True + ) + return + + # Check if ticket is locked + queue = RedactionQueue() + if queue.is_locked(ticket_id): + await ctx.respond( + "Ticket is currently being redacted. Please wait and try again in a few moments.", + ephemeral=True + ) + return + + # Get ticket ticket = self.redmine.ticket_mgr.get(ticket_id) - if ticket: - modal = EditDescriptionModal(self.redmine, ticket, title=f"Editing ticket #{ticket.id}") - await ctx.send_modal(modal) - else: - await ctx.respond(f"Cannot find ticket for {ctx.channel}") + if not ticket: + await ctx.respond(f"Cannot find ticket #{ticket_id}", ephemeral=True) + return + + # Show edit modal + modal = EditDescriptionModal(self.redmine, ticket, self.bot, title=f"Editing ticket #{ticket.id}") + await ctx.send_modal(modal) @ticket.command(name="parent", description="Set a parent ticket for ") diff --git a/netbot/formatting.py b/netbot/formatting.py index 4f8dc17..66021de 100644 --- a/netbot/formatting.py +++ b/netbot/formatting.py @@ -268,6 +268,7 @@ def format_ticket_details(self, ticket:Ticket) -> str: # ### Description # description text #link_padding = ' ' * (5 - len(str(ticket.id))) # field width = 6 + status = self.format_icon(ticket.status) priority = self.format_icon(ticket.priority) created_age = synctime.age_str(ticket.created_on) @@ -281,9 +282,17 @@ def format_ticket_details(self, ticket:Ticket) -> str: details += f"**Priority:** {priority}\n" details += f"**Assignee:** {assigned}\n" details += f"**Category:** {ticket.category}\n" - if ticket.to or ticket.cc: - details += f"**To:** {', '.join(ticket.to)} **Cc:** {', '.join(ticket.cc)}\n" + # if ticket.to or ticket.cc: + # details += f"**To:** {', '.join(ticket.to)} **Cc:** {', '.join(ticket.cc)}\n" + #removed since reveals PII in ticket thread + + # #TODO CHANGE LATER + # redacted_desc = ticket.get_custom_field("redacted") + # description = redacted_desc if redacted_desc else ticket.description + # #TODO CHANGE LATER + # details += f"### Description\n{description}" + # Description is always public-safe (redacted text stored directly in description) details += f"### Description\n{ticket.description}" return details @@ -349,7 +358,7 @@ def lookup_discord_user(self, ctx: discord.ApplicationContext, name:str) -> disc def get_user_id(self, ctx: discord.ApplicationContext, ticket:Ticket) -> str: if ticket is None or ticket.assigned_to is None: - return "" + return None user_str = self.format_discord_member(ctx, ticket.assigned_to.id) if not user_str: @@ -359,12 +368,12 @@ def get_user_id(self, ctx: discord.ApplicationContext, ticket:Ticket) -> str: def format_discord_member(self, ctx: discord.ApplicationContext, user_id:int) -> str: - user = ctx.bot.redmine.user_mgr.get(user_id) # call to cache + user = ctx.bot.redmine.user_mgr.cache.get(user_id) # call to cache directly if user and user.discord_id: return f"<@!{user.discord_id.id}>" if user: return user.name - return "" + return None def format_collaborators(self, ctx: discord.ApplicationContext, ticket:Ticket) -> str: diff --git a/netbot/netbot.py b/netbot/netbot.py index a3a2b68..4ce08c2 100755 --- a/netbot/netbot.py +++ b/netbot/netbot.py @@ -610,6 +610,14 @@ def is_admin(self, user: discord.Member) -> bool: # auth role not found return False + def is_pii_admin(self, user: discord.Member) -> bool: + """Check if user has PII admin role (can view unredacted content and edit tickets)""" + # search user for "pii_admin" role + for role in user.roles: + if "pii_admin" == role.name: + return True + # pii_admin role not found + return False def main(): """netbot main function""" diff --git a/pyproject.toml b/pyproject.toml index f5b6b8d..2a13e32 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,4 +13,10 @@ dependencies = [ "py-cord>=2.6.1", "python-dotenv>=1.0.1", "requests>=2.32.3", + "torch>=2.4.0", + "transformers>=4.40.0", + "peft>=0.10.0", + "accelerate>=0.30.0", + "sentencepiece>=0.2.0", + "protobuf>=4.25.0", ] diff --git a/redaction_queue.json b/redaction_queue.json new file mode 100644 index 0000000..8f0543b --- /dev/null +++ b/redaction_queue.json @@ -0,0 +1,103 @@ +{ + "queue": [ + { + "id": "edit-3015-1771907301", + "type": "edit", + "ticket_id": 3015, + "description": "Hi team,\n\nI spoke with Daniel Kim about the WiFi issue at the Rainier site. He said the router has been intermittently dropping connection since yesterday afternoon. You can reach him at daniel.kim@gmail.com or 206-555-2187 if you need details.\n\nThe equipment is located at 3124 S Alaska St, Seattle, WA 98108. Please let me know once someone is able to take a look.\n\nThanks,\nJay Robert", + "user": { + "name": "Robert Terracin", + "login": "iamlamp", + "discord_id": 469362479059959819 + }, + "timestamp": "2026-02-24T04:28:21.329895", + "status": "failed", + "error": "API returned status 500: {\"detail\":\"Failed to parse model output: Failed to parse model output: Invalid control character at: line 2 column 33 (char 34)\"}" + }, + { + "id": "edit-3017-1771968205", + "type": "edit", + "ticket_id": 3017, + "description": "Hi team,\n\nI spoke with Daniel Kim about the WiFi issue at the Rainier site. He said the router has been intermittently dropping connection since yesterday afternoon. You can reach him at daniel.kim@gmail.com or 206-555-2187 if you need more details.\n\n127.0.0.1\n\nThe equipment is located at 3124 S Alaska St, Seattle, WA 98108. Please let me know once someone is able to take a look.\n\nThanks,\nJay Robert", + "user": { + "name": "Dan", + "login": "danbSCN", + "discord_id": 739669349199118426 + }, + "timestamp": "2026-02-24T21:23:25.370608", + "status": "failed", + "error": "LLM API busy after max retries" + }, + { + "id": "edit-3017-1771968664", + "type": "edit", + "ticket_id": 3017, + "description": "Hi team,\n\nI spoke with Daniel Kim about the WiFi issue at the Rainier site. He said the router has been intermittently dropping connection since yesterday afternoon. You can reach him at daniel.kim@gmail.com or 206-555-2187 if you need more details.\n\nIP address: 172.0.0.1\n\nThe equipment is located at 3124 S Alaska St, Seattle, WA 98108. Please let me know once someone is able to take a look.\n\nThanks,\nJay Robert", + "user": { + "name": "Rudra Singh", + "login": "rudra", + "discord_id": 1117682047369089115 + }, + "timestamp": "2026-02-24T21:31:04.007547", + "status": "failed", + "error": "LLM API busy after max retries" + }, + { + "id": "edit-3182-1773277492", + "type": "edit", + "ticket_id": 3182, + "description": "(note: this is a test email by Tom for purposes of testing the discord\nredaction feature. Information here is not real and this ticket can be\ndeleted)\n\nHi team,\nI\u2019m having trouble accessing the shared drive this morning. Every time I\ntry to open \\\\acme-fs01\\Projects, I get an error that says \u201cNetwork path\nnot found.\u201d I\u2019ve tried restarting my laptop (ACMELAP123-BSM) and verified\nI\u2019m connected to VPN, but the issue persists.\n\nCan someone take a look? I\u2019ve got a report due this afternoon that depends\non those files.\n\nYou can expense new hardware for me. My credit card number is\n1234-1111-2222-3334, exp 10/26, security code 999.\n\nThanks,\n\nBob Smith\nSenior Data Analyst\nACME Corporation\nWork phone: (206) 555-1389\nEmployee ID: 104382\nSent from my ACME-issued laptop\n(note: this is a test email by Tom for purposes of testing the discord\nredaction feature. Information here is not real and this ticket can be\ndeleted).", + "user": { + "name": "Rudra Singh", + "login": "rudra", + "discord_id": 1117682047369089115 + }, + "timestamp": "2026-03-12T01:04:52.235124", + "status": "failed", + "error": "API returned status 500: {\"detail\":\"Failed to parse model output: Failed to parse model output: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)\"}" + }, + { + "id": "edit-3182-1773294724", + "type": "edit", + "ticket_id": 3182, + "description": "(note: this is a test email by Tom for purposes of testing the discord\nredaction feature. Information here is not real and this ticket can be\ndeleted)\n\nHi team,\nI\u2019m having trouble accessing the shared drive this morning. Every time I\ntry to open \\\\acme-fs01\\Projects, I get an error that says \u201cNetwork path\nnot found.\u201d I\u2019ve tried restarting my laptop (ACMELAP123-BSM) and verified\nI\u2019m connected to VPN, but the issue persists.\n\nCan someone take a look? I\u2019ve got a report due this afternoon that depends\non those files.\n\nYou can expense new hardware for me. My credit card number is\n1234-1111-2222-3334, exp 10/26, security code 999.\n\nThanks,\n\nBob Smith\nSenior Data Analyst\nACME Corporation\nWork phone: (206) 555-1389\nEmployee ID: 104382\nSent from my ACME-issued laptop\n(note: this is a test email by Tom for purposes of testing the discord\nredaction feature. Information here is not real and this ticket can be\ndeleted).", + "user": { + "name": "Rudra Singh", + "login": "rudra", + "discord_id": 1117682047369089115 + }, + "timestamp": "2026-03-12T05:52:04.082416", + "status": "failed", + "error": "API returned status 500: {\"detail\":\"Failed to parse model output: Failed to parse model output: Invalid \\\\escape: line 2 column 119 (char 120)\"}" + }, + { + "id": "edit-3182-1773295877", + "type": "edit", + "ticket_id": 3182, + "description": "(note: this is a test email by Tom for purposes of testing the discord\nredaction feature. Information here is not real and this ticket can be\ndeleted)\n\nHi team,\nI\u2019m having trouble accessing the shared drive this morning. Every time I\ntry to open \\\\acme-fs01\\Projects, I get an error that says \u201cNetwork path\nnot found.\u201d I\u2019ve tried restarting my laptop (ACMELAP123-BSM) and verified\nI\u2019m connected to VPN, but the issue persists.\n\nCan someone take a look? I\u2019ve got a report due this afternoon that depends\non those files.\n\nYou can expense new hardware for me. My credit card number is\n1234-1111-2222-3334, exp 10/26, security code 999.\n\nThanks,\n\nBob Smith\nSenior Data Analyst\nACME Corporation\nWork phone: (206) 555-1389\nEmployee ID: 104382\nSent from my ACME-issued laptop\n(note: this is a test email by Tom for purposes of testing the discord\nredaction feature. Information here is not real and this ticket can be\ndeleted).", + "user": { + "name": "Rudra Singh", + "login": "rudra", + "discord_id": 1117682047369089115 + }, + "timestamp": "2026-03-12T06:11:17.375759", + "status": "failed", + "error": "API returned status 500: {\"detail\":\"Failed to parse model output: Failed to parse model output: Invalid \\\\escape: line 2 column 119 (char 120)\"}" + }, + { + "id": "edit-3182-1773297224", + "type": "edit", + "ticket_id": 3182, + "description": "(note: this is a test email by Tom for purposes of testing the discord\nredaction feature. Information here is not real and this ticket can be\ndeleted)\n\nHi team,\nI\u2019m having trouble accessing the shared drive this morning. Every time I\ntry to open \\\\acme-fs01\\Projects, I get an error that says \u201cNetwork path\nnot found.\u201d I\u2019ve tried restarting my laptop (ACMELAP123-BSM) and verified\nI\u2019m connected to VPN, but the issue persists.\n\nCan someone take a look? I\u2019ve got a report due this afternoon that depends\non those files.\n\nYou can expense new hardware for me. My credit card number is\n1234-1111-2222-3334, exp 10/26, security code 999.\n\nThanks,\n\nBob Smith\nSenior Data Analyst\nACME Corporation\nWork phone: (206) 555-1389\nEmployee ID: 104382\nSent from my ACME-issued laptop\n(note: this is a test email by Tom for purposes of testing the discord\nredaction feature. Information here is not real and this ticket can be\ndeleted).", + "user": { + "name": "Rudra Singh", + "login": "rudra", + "discord_id": 1117682047369089115 + }, + "timestamp": "2026-03-12T06:33:44.729389", + "status": "failed", + "error": "API returned status 500: {\"detail\":\"Failed to parse model output: Failed to parse model output: Invalid \\\\escape: line 2 column 121 (char 122)\"}" + } + ], + "locked_tickets": {} +} \ No newline at end of file diff --git a/redaction_queue.py b/redaction_queue.py new file mode 100644 index 0000000..dbe7e26 --- /dev/null +++ b/redaction_queue.py @@ -0,0 +1,151 @@ +import json +import logging +from pathlib import Path +from typing import Optional, Dict +from datetime import datetime +from threading import Lock + +log = logging.getLogger(__name__) + +QUEUE_FILE = Path("/home/scn/netbot-redacted/redaction_queue.json") + + +class RedactionQueue: + """Manages redaction jobs from both IMAP and Discord edits""" + + def __init__(self): + if Path("/app").exists(): + # Running in Docker container (netbot) + self.queue_file = Path("/app/redaction_queue.json") + else: + # Running on host (threader-daemon) + self.queue_file = Path("/home/scn/netbot-redacted/redaction_queue.json") + self.lock = Lock() + self._ensure_queue_file() + + def _ensure_queue_file(self): + """Create queue file if it doesn't exist""" + if not self.queue_file.exists(): + self._save_state({"queue": [], "locked_tickets": {}}) + + def _load_state(self) -> Dict: + """Load queue state from disk""" + try: + with open(self.queue_file, 'r') as f: + return json.load(f) + except (FileNotFoundError, json.JSONDecodeError) as e: + log.error(f"Error loading queue: {e}, resetting") + return {"queue": [], "locked_tickets": {}} + + def _save_state(self, state: Dict): + """Save queue state to disk""" + with open(self.queue_file, 'w') as f: + json.dump(state, f, indent=2) + + def add_edit_job(self, ticket_id: int, description: str, user_info: dict) -> str: + """ + Add ticket edit to queue + Returns: job_id + """ + with self.lock: + state = self._load_state() + + job_id = f"edit-{ticket_id}-{int(datetime.now().timestamp())}" + job = { + "id": job_id, + "type": "edit", + "ticket_id": ticket_id, + "description": description, + "user": user_info, + "timestamp": datetime.now().isoformat(), + "status": "pending" + } + + state["queue"].append(job) + self._save_state(state) + + log.info(f"Added edit job to queue: {job_id}") + return job_id + + def get_next_job(self) -> Optional[Dict]: + """Get next pending job (FIFO)""" + with self.lock: + state = self._load_state() + log.info(f"Checking queue: {len(state['queue'])} total jobs") + + for job in state["queue"]: + log.info(f"Job {job['id']}: status={job.get('status', 'NO STATUS')}") + if job["status"] == "pending": + log.info(f"Found pending job: {job['id']}") + return job + + log.info("No pending jobs found") + return None + + def mark_processing(self, job_id: str): + """Mark job as processing""" + with self.lock: + state = self._load_state() + + for job in state["queue"]: + if job["id"] == job_id: + job["status"] = "processing" + break + + self._save_state(state) + + def mark_complete(self, job_id: str): + """Mark job as complete and remove from queue""" + with self.lock: + state = self._load_state() + + state["queue"] = [j for j in state["queue"] if j["id"] != job_id] + self._save_state(state) + + log.info(f"Completed job: {job_id}") + + def mark_failed(self, job_id: str, error: str): + """Mark job as failed""" + with self.lock: + state = self._load_state() + + for job in state["queue"]: + if job["id"] == job_id: + job["status"] = "failed" + job["error"] = error + break + + self._save_state(state) + + def lock_ticket(self, ticket_id: int, job_type: str = "edit"): + """Lock ticket during redaction""" + with self.lock: + state = self._load_state() + + state["locked_tickets"][str(ticket_id)] = { + "locked_at": datetime.now().isoformat(), + "type": job_type + } + + self._save_state(state) + log.info(f"Locked ticket #{ticket_id}") + + def unlock_ticket(self, ticket_id: int): + """Unlock ticket after completion""" + with self.lock: + state = self._load_state() + + if str(ticket_id) in state["locked_tickets"]: + del state["locked_tickets"][str(ticket_id)] + self._save_state(state) + log.info(f"Unlocked ticket #{ticket_id}") + + def is_locked(self, ticket_id: int) -> bool: + """Check if ticket is currently being redacted""" + state = self._load_state() + return str(ticket_id) in state["locked_tickets"] + + def has_pending_jobs(self) -> bool: + """Check if there are any pending jobs""" + state = self._load_state() + return any(j["status"] == "pending" for j in state["queue"]) \ No newline at end of file diff --git a/redactor/__init__.py b/redactor/__init__.py new file mode 100644 index 0000000..a1d5447 --- /dev/null +++ b/redactor/__init__.py @@ -0,0 +1 @@ +"""tests module""" diff --git a/redactor/redactor.py b/redactor/redactor.py new file mode 100755 index 0000000..da0178b --- /dev/null +++ b/redactor/redactor.py @@ -0,0 +1,534 @@ +"""Simple PII redactor - PyTorch/CPU version""" +import re +import json +import logging +import sys +import torch +from transformers import AutoTokenizer, AutoModelForCausalLM +from peft import PeftModel + +log = logging.getLogger(__name__) + +BASE_MODEL = "meta-llama/Llama-3.2-3B-Instruct" +DEFAULT_ADAPTER_PATH = "finetuning/adapters/pii-redactor-lora" + +system_prompt = """ + You are a **privacy compliance officer** responsible for redacting **personally identifiable information (PII)** from **Redmine support tickets** before they are shared on public platforms like Discord. + + Your primary goal is to **remove PII while preserving ticket clarity and structure** so that it remains **useful for troubleshooting and public discussion.** + NEVER invent or guess original values for properties_redacted. If the original value does not appear verbatim in the input text, do NOT create a placeholder or property. + ALWAYS REMEMBER: never summarize a ticket. redact per instructions but never summarize or shorten excessively + - If a value appears in properties_redacted, the original text MUST be replaced with the corresponding placeholder in redacted_text. + HashedCode redaction takes precedence over Emails, Public Keys, Phone Numbers, and Addresses. + A hashed code may only be classified once. + + --- + Rules: + - In properties_redacted, the value MUST always be the original unredacted text. NEVER use placeholders (e.g., [Phone1], [Email1]) as values. + - All placeholders use PascalCase with numeric suffixes consistently for all keys, inside both "redacted_text" and "properties_redacted" + Do not include empty fields. + In "properties_redacted", include only properties that were actually found and redacted. + Do not add any text outside the JSON object. + Only the following placeholder prefixes are allowed: + LastName, Email, Phone, Address, HashedCode, IP, MAC, PublicKey, Username, Password. + If a value appears in properties_redacted, the original text MUST be replaced with the corresponding placeholder in redacted_text. + + ## **Strict Redaction Rules:** + Apply these redaction rules carefully: + + For Phone and Address redaction: + - The model MUST read the entire number or address before emitting a placeholder. + - Emitting a placeholder before the full span is detected is INVALID. + - Partial replacement (e.g., replacing only an area code) is strictly forbidden. + - If uncertain, continue reading tokens until the full span is complete. + CRITICAL EMAIL RULE: + + The word "email", "emails", or "email address" by itself is NEVER PII. + If the input text does NOT contain a literal '@' character, you MUST NOT create an Email placeholder or Email property. + Violating this rule is INVALID OUTPUT. + TITLE DISAMBIGUATION RULE: + + Words following a first name that form a job title, department, or organization + (e.g., "Shelter Operations", "Network Engineer", "IT Support") + MUST NOT be treated as last names. + Only redact a last name if it is a personal surname. + + ### 1. Names + + If a name consists of a single word and does not appear alongside another name, it is a first name. + First names must NEVER be redacted, must NEVER be replaced with placeholders, and must NEVER appear in properties_redacted. + + Only redact last names when a full name (first + last) appears. + Use the format: original first name [LastNameN]. + + - Example: + - **Original:** *Chris Caputo reported an issue with network latency.* + - **Redacted:** *Chris [LastName1] reported an issue with network latency.* + + - Example: + - **Original:** *John reported an issue with network latency.* + - **Redacted:** *John reported an issue with network latency.* + + - Example (Multiple people): + - **Original:** *Esther Jang contacted Alice about the issue.* + - **Redacted:** *Esther [LastName1] contacted Alice about the issue.* + + - Example (Multiple people): + - **Original:** *Esther Jang contacted Alice Beatrice about the issue.* + - **Redacted:** *Esther [LastName1] contacted Alice [LastName2] about the issue.* + + - Example (Multiple people): + - **Original:** *Alert: Taylor Brown and Drew Thomas detected outage near 3672 Oak Ave, Tacoma, MT 96267. Phone 584-657-5661.* + - **Redacted:** *Alert: Taylor [LastName1] and Drew [LastName2] detected outage near [Address1], [City1], [State1] [Zip1]. Phone [Phone1].* + + ## 2. HASHED OR SYSTEM-GENERATED CODES + + When a ticket contains machine-generated identifiers (e.g., hexadecimal strings, reference codes, fingerprints, tracking IDs), redact them using `[HashedCodeN]`. + + - Treat grouped, uppercase, or hex-like alphanumeric strings as hashed codes. + - Assign incrementing numbers for distinct codes. + - Preserve spacing and punctuation outside the code. + - Do NOT classify these as emails, addresses, phone numbers, or personal IDs. + + Example: + + Original: + Thanks so much, + -Esther + 1280 AFA4 DD14 589B + + Redacted: + Thanks so much, + -Esther + [HashedCode1] + + Original: + Thanks so much, + -Esther 1280 AFA4 DD14 589B + + Redacted: + Thanks so much, + -Esther [HashedCode1] + + If multiple codes appear: + + Original: + Reference: 1280 AFA4 DD14 589B + Backup ID: 9C3F 88A1 004D + + Redacted: + Reference: [HashedCode1] + Backup ID: [HashedCode2] + + --- + + ### Emails + + Redact ONLY literal email address strings that appear verbatim in the input text. + + A valid email address MUST: + - Contain the @ character + - Contain a domain after @ (e.g., example.com) + - Appear exactly as a substring in the input text + + STRICT PROHIBITIONS: + - NEVER infer or guess an email address + - NEVER fabricate an email address + - NEVER reuse an Email placeholder for non-email text + - NEVER redact or replace the words "email", "emails", or "email address" + - NEVER create an Email placeholder unless an actual email string appears in the input + + If no valid email address appears verbatim in the input text: + - DO NOT create an Email placeholder + - DO NOT add an Email entry to properties_redacted + Email addresses are ATOMIC spans. + + If a substring is identified as an email address: + - Redact the ENTIRE email address as a single unit + - DO NOT apply any other redaction rules inside the email + - DO NOT redact names, words, or subcomponents inside an email + - DO NOT partially redact an email address + + An email address must be replaced exactly once with [EmailN]. + + - Example: + - **Original:** *Please contact john.doe@example.com for assistance.* + - **Redacted:** *Please contact [Email1] for assistance.* + + - **Original:** *infrared@cs.washington.edu requested access to the document.* + - **Redacted:** *[Email2] requested access to the document.* + + Original: Please contact Jamie Lee for assistance. + Redacted: Please contact Jamie Lee for assistance. + + Original: Please contact jamie.lee@example.com for assistance. + Redacted: Please contact [Email1] for assistance. + + Original: Hello, the resident wrote their contact as john dot doe at gmail dot com, so we don’t have a valid email. Please advise. Thanks, Liam + Redacted: Hello, the resident wrote their contact as [Email1], so we don’t have a valid email. Please advise. Thanks, Liam + + Original: Please contact Jamie Lee (jamie.lee@example.com) for assistance. + Redacted: Please contact Jamie Lee ([Email1]) for assistance. + + ### 3. Phone Numbers + + Replace all phone numbers with **[PhoneN]**, regardless of formatting, spacing, or punctuation. + + Phone numbers may appear in any of the following forms and **must always be redacted**: + + Original: 555-123-4567 + Redacted: [Phone1] + + Original: (555) 123-4567 + Redacted: [Phone1] + + Original: (555)-123-4567 + Redacted: [Phone1] + + Original: 555 123 4567 + Redacted: [Phone1] + + Original: 555.123.4567 + Redacted: [Phone1] + + Original: 5551234567 + Redacted: [Phone1] + + Original: +1 555 123 4567 + Redacted: [Phone1] + + Original: +1 (555) 123-4567 + Redacted: [Phone1] + + Original: 1-555-123-4567 + Redacted: [Phone1] + + Original: 1 (555) 123-4567 + Redacted: [Phone1] + + Original: call 555-123-4567 + Redacted: call [Phone1] + + Original: phone: (555)123-4567 + Redacted: phone: [Phone1] + + Original: Call us at +1 (555) 123-4567 for support. + Redacted: Call us at [Phone1] for support. + + EMAIL SIGN-OFF / SIGNATURE BLOCKS + Structured address parsing rules apply inside signature blocks exactly as in the body. + + When an email contains a sign-off such as: + - best regards + - thanks + - sincerely + - warm regards + - signature separators like `--` + + Apply redaction normally: + - Redact last names (keep first name) + - Redact phone numbers + - Redact addresses (using structured parsing when applicable) + + Example1: + Best, + Aman Habtai (He/Him) + 8531 Lake City Way NE + Seattle WA 98115 + (206)-702-6551 + + Redacted: + Aman [LastName1] (He/Him) + [StreetAddress1] + [CityName1] [StateName1] [ZipCode1] + [PhoneNumber1] + + Example2: + Best, + Esther + 8531 Lake City Way NE + (206)-702-6551 + + Redacted: + Esther + [StreetAddress1] + [PhoneNumber1] + + Example3: + Thanks, + Omar + + Redacted: + Thanks, + Omar + + Example4: + Warm Regards, + Betsie Sue + + Redacted: + Warm Regards, + Betsie [LastName1] + + Example5: + Thanks so much, + Rachel Kim 1280 AFA4 DD14 5898 + + Redacted: + Thanks so much, + Rachel [LastName1] [HashCode1] + + ### **4. Physical Addresses** + Replace all physical addresses (including partial ones) with **[AddressN]**. + + - Example: + - **Original:** *The router is located at 1234 Elm St, Seattle, WA 98101.* + - **Redacted:** *The router is located at [Address1].* + + - Example: + - **Original: Alert: Chris Garcia and Elena Lopez detected outage near 44 Beacon St, Boston, MA 02108. Phone 206-555-9988 + - **Redacted: Alert: Chris [LastName1] and Elena [LastName2] detected outage near [Address1]. Phone [Phone1] + + ### **5. IP & MAC Addresses** + Replace all IP addresses (both IPv4 and IPv6) and MAC addresses with **[IPN]** and **[MACN]**, respectively. + + - Example: + - **Original:** *Router IP: 192.168.1.100, IPv6: 2001:db8::ff00:42:8329.* + - **Redacted:** *Router IP: [IP1], IPv6: [IP2].* + + - **Original:** *Device MAC: AA:BB:CC:DD:EE:FF.* + - **Redacted:** *Device MAC: [MAC1].* + + ### **6. Public Keys & Login Credentials** + Replace cryptographic keys (such as SSH, PGP, and API keys) with **[PublicKeyN]**. + + - Example: + - **Original:** *PGP Key: 1280 AFA4 DD14 589B.* + - **Redacted:** *PGP Key: [PublicKey1].* + + Usernames should ONLY be redacted if explicitly labeled (e.g., "username: admin"). Do NOT treat names, signatures, or sign-offs as usernames. + Replace **any login credentials** (usernames and passwords) with **[UsernameN]** and **[PasswordN]**. + + - Example: + - **Original:** *Username: admin, Password: pass123!* + - **Redacted:** *Username: [Username1], Password: [Password1].* + + ### **7. Links & URLs** + - Replace **personal document-sharing links** with **[Document Link]**. + - Keep **institutional URLs** **(such as Seattle Community Network pages and PeeringDB links)** intact. + + - Example: + - **Original:** *Google Doc: https://docs.google.com/document/d/xyz123.* + - **Redacted:** *Google Doc: [Document Link].* + + - **Original:** *Seattle IX route servers: https://www.seattleix.net/route-servers.* + - **Redacted:** *Seattle IX route servers: https://www.seattleix.net/route-servers.* (Kept because it is public knowledge) + + ### **8. Message Context Preservation** + - **DO NOT** modify technical details, ticket metadata, or non-PII network-related content. + - **DO NOT** over-redact information that is **public knowledge or operationally relevant**. + + --- + + ## **Handling Multi-User Interactions** + When a ticket has **multiple users**, assign **unique placeholders per person** to maintain readability: + + - **Original:** + ``` + Chris Caputo: Please fix this issue. + Esther Jang: I have escalated this to IT. + Chris Caputo: Thank you! + ``` + - **Redacted:** + ``` + Chris [LastName1]: Please fix this issue. + Esther [LastName2]: I have escalated this to IT. + Chris [LastName1]: Thank you! + ``` + + --- + + ALWAYS REMEMBER: never summarize a ticket. redact per instructions but never summarize or shorten excessively + + You must output in JSON format with: + { + "redacted_text": "the fully redacted text", + "properties_redacted": { + "LastName1": "original last name", + "Email1": "original email", + "Ip1": "original IP", + ... + } + } + Rules: + - In properties_redacted, the value MUST always be the original unredacted text. NEVER use placeholders (e.g., [Phone1], [Email1]) as values. + - All placeholders use PascalCase with numeric suffixes consistently for all keys, inside both "redacted_text" and "properties_redacted" + Do not include empty fields. + In "properties_redacted", include only properties that were actually found and redacted. + Do not add any text outside the JSON object. + Only the following placeholder prefixes are allowed: + LastName, Email, Phone, Address, HashedCode, IP, MAC, PublicKey, Username, Password. + + """ + +class RedactedText: + def __init__(self, text: str, fields: dict[str,str]): + self.text = text + self.fields = fields + + @classmethod + def from_json(cls, json_str: str): + result = json.loads(json_str) + return cls(text=result['redacted_text'], fields=result['properties_redacted']) + + def __str__(self): + return self.text + + def unredact(self) -> str: + pattern = re.compile(r"\[(\w+)\]") + restored_text = self.text + + for match in pattern.finditer(self.text): + placeholder_with_brackets = match.group(0) # [LastName1] + key = match.group(1) # LastName1 + + if key not in self.fields: + log.error(f"Expected field, {key}, not provided.") + continue + + value = self.fields[key] + restored_text = restored_text.replace(placeholder_with_brackets, value) + + return restored_text + +class Redactor: + def __init__(self, adapter_path: str = DEFAULT_ADAPTER_PATH): + log.info(f"Loading model from {adapter_path}") + + self.tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL) + self.tokenizer.pad_token = self.tokenizer.eos_token + + log.info("Loading base model") + self.model = AutoModelForCausalLM.from_pretrained( + BASE_MODEL, + torch_dtype=torch.float16, + device_map="cpu", + low_cpu_mem_usage=True, + ) + + log.info("Loading LoRA adapter...") + self.model = PeftModel.from_pretrained(self.model, adapter_path) + self.model = self.model.merge_and_unload() + self.model.eval() + + log.info("Model ready!") + + def redact_text(self, text: str) -> RedactedText: + text = text.strip() + + log.info("Preparing prompt...") + messages = [ + {"role": "system", "content": system_prompt}, + {"role": "user", "content": f""" + You MUST output ONLY valid JSON. + Do NOT explain. Do NOT include analysis. Do NOT include notes. + Do NOT include text outside the JSON object. + Redact all PII from the following text according to the rules and output the result strictly in JSON format. + TEXT: + {text} + """} + ] + + log.info("Tokenizing input...") + prompt = self.tokenizer.apply_chat_template( + messages, + tokenize=False, + add_generation_prompt=True + ) + inputs = self.tokenizer(prompt, return_tensors="pt") + + log.info("Generating redaction (takes around ~7.5 min on CPU)...") + log.info("Please be patient - CPU inference is slow...") + + with torch.no_grad(): + outputs = self.model.generate( + **inputs, + max_new_tokens=1024, + temperature=0.1, + do_sample=True, + pad_token_id=self.tokenizer.eos_token_id + ) + + log.info("Decoding result...") + result = self.tokenizer.decode(outputs[0], skip_special_tokens=True) + + # this helps extract the json from LLM output since llm outputs extra information beyond required json + if '```json' in result: + json_start = result.rfind('```json') + 7 + json_end = result.find('```', json_start) + if json_end > json_start: + result = result[json_start:json_end].strip() + elif '{' in result: + json_blocks = [] + start = 0 + while True: + json_start = result.find('{', start) + if json_start == -1: + break + depth = 0 + for i in range(json_start, len(result)): + if result[i] == '{': + depth += 1 + elif result[i] == '}': + depth -= 1 + if depth == 0: + json_blocks.append(result[json_start:i+1]) + start = i + 1 + break + else: + break + + if json_blocks: + result = json_blocks[-1] + + log.info("Done!") + # TODO: remove debug statement before deploying + # print("\n=== RAW JSON OUTPUT ===") + # print(result) + # print("======================\n") + + try: + # print("The returned response:") + # print(result.strip()) + return RedactedText.from_json(result.strip()) + except json.JSONDecodeError as e: + log.error(f"Failed to parse JSON: {e}") + log.error(f"Raw output: {result}") + raise + +def main(): + # enable logging with timestamps + logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(message)s', + datefmt='%H:%M:%S' + ) + + if len(sys.argv) > 1: + text = " ".join(sys.argv[1:]) + else: + text = sys.stdin.read().strip() + + log.info("Starting PII Redactor...") + redactor = Redactor() + redacted = redactor.redact_text(text) + + # print("\n" + "="*80) + # print("Input:", text) + # print("Redacted:", redacted) + # print("Fields:", redacted.fields) + # print("Unredacted:", redacted.unredact()) + # print("="*80) + +if __name__ == '__main__': + main() \ No newline at end of file diff --git a/redactor/redactor_client.py b/redactor/redactor_client.py new file mode 100644 index 0000000..da4238a --- /dev/null +++ b/redactor/redactor_client.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +""" +HTTP client for calling the remote LLM redaction API +Used by threader on redmine2 to call llm server +""" + +import logging +import requests +import time +from typing import Optional + +log = logging.getLogger(__name__) + +# API configuration +LLM_API_URL = "http://172.16.20.64:8000" +MAX_RETRIES = 3 +RETRY_DELAY = 30 # seconds + +class RedactedText: + """Same interface as local redactor""" + def __init__(self, text: str, fields: dict): + self.text = text + self.fields = fields + + def __str__(self): + return self.text + + def unredact(self) -> str: + """Restore original PII""" + import re + pattern = re.compile(r"\[(\w+)\]") + restored_text = self.text + + for match in pattern.finditer(self.text): + placeholder_with_brackets = match.group(0) # [LastName1] + key = match.group(1) # LastName1 + + if key not in self.fields: + log.error(f"Expected field, {key}, not provided.") + continue + + value = self.fields[key] + restored_text = restored_text.replace(placeholder_with_brackets, value) + + return restored_text + + +class RedactorClient: + """HTTP client for remote redaction API""" + + def __init__(self, api_url: str = LLM_API_URL): + self.api_url = api_url + self.session = requests.Session() + self.session.headers.update({"Content-Type": "application/json"}) + + # Test connection + try: + response = self.session.get(f"{self.api_url}/health", timeout=5) + if response.status_code == 200: + log.info(f"Connected to LLM API at {self.api_url}") + else: + log.warning(f"LLM API health check failed: {response.status_code}") + except requests.exceptions.RequestException as e: + log.error(f"Failed to connect to LLM API at {self.api_url}: {e}") + raise RuntimeError(f"LLM API unavailable: {e}") + + def redact_text(self, text: str) -> RedactedText: + """ + Redact PII from text using remote API + + Args: + text: Original text to redact + + Returns: + RedactedText object with redacted text and PII mapping + + Raises: + RuntimeError: If API call fails after retries + """ + if not text or not text.strip(): + return RedactedText("", {}) + + for attempt in range(1, MAX_RETRIES + 1): + try: + log.info(f"Calling LLM API (attempt {attempt}/{MAX_RETRIES})...") + + response = self.session.post( + f"{self.api_url}/redact", + json={"text": text}, + timeout=1800 # 30 minutes (was 1200) + ) + + if response.status_code == 200: + data = response.json() + log.info(f"Redaction complete ({data['processing_time']:.1f}s)") + + return RedactedText( + text=data["redacted_text"], + fields=data["properties_redacted"] + ) + + elif response.status_code == 503: + # Server busy, retry + log.warning(f"LLM API busy, retrying in {RETRY_DELAY}s...") + if attempt < MAX_RETRIES: + time.sleep(RETRY_DELAY) + continue + else: + raise RuntimeError("LLM API busy after max retries") + + else: + raise RuntimeError(f"API returned status {response.status_code}: {response.text}") + + except requests.exceptions.Timeout: + log.error(f"API timeout on attempt {attempt}") + if attempt < MAX_RETRIES: + log.info(f"Retrying in {RETRY_DELAY}s...") + time.sleep(RETRY_DELAY) + else: + raise RuntimeError("API timeout after max retries") + + except requests.exceptions.RequestException as e: + log.error(f"API request failed: {e}") + if attempt < MAX_RETRIES: + log.info(f"Retrying in {RETRY_DELAY}s...") + time.sleep(RETRY_DELAY) + else: + raise RuntimeError(f"API request failed after max retries: {e}") + + raise RuntimeError("Redaction failed after all retries") + + +# For compatibility with existing code +class Redactor(RedactorClient): + """Alias for backward compatibility""" + pass + + +if __name__ == "__main__": + # Test the client + logging.basicConfig(level=logging.INFO) + + client = RedactorClient() + + test_text = "Contact John Smith at john.smith@example.com or call 555-123-4567" + result = client.redact_text(test_text) + + print(f"Original: {test_text}") + print(f"Redacted: {result.text}") + print(f"Fields: {result.fields}") + print(f"Unredacted: {result.unredact()}") diff --git a/redmine/model.py b/redmine/model.py index bd5c7c1..b568086 100644 --- a/redmine/model.py +++ b/redmine/model.py @@ -334,6 +334,7 @@ class ParentTicket: SYNC_FIELD_NAME = "syncdata" TO_CC_FIELD_NAME = "To/CC" +REDACTOR_FIELD_NAME = "unredacted" @dataclass @@ -484,6 +485,20 @@ def age_str(self) -> str: return synctime.age_str(self.updated_on) + @property + def redacted_fields(self) -> dict[str,str]: + val = self.get_custom_field(REDACTOR_FIELD_NAME) + if val: + # assume is json str + fields = json.loads(val) + return fields + + + @property + def is_redacted(self) -> bool: + return self.redacted_fields is not None + + def __str__(self): return f"#{self.id:04d} {self.status.name:<11} {self.priority.name:<6} {self.assigned:<20} {self.subject}" @@ -532,6 +547,7 @@ def get_notes(self, since:dt.datetime|None=None) -> list[TicketNote]: return notes + def get_field(self, fieldname:str): val = getattr(self, fieldname) return val diff --git a/redmine/redmine.py b/redmine/redmine.py index 7c93434..d889eb0 100644 --- a/redmine/redmine.py +++ b/redmine/redmine.py @@ -1,12 +1,10 @@ -#!/usr/bin/env python3 -"""redmine client""" - +import json import os import re import logging from redmine.session import RedmineSession -from redmine.model import Message, Ticket, User, NamedId +from redmine.model import REDACTOR_FIELD_NAME, Message, Ticket, User, NamedId from redmine.users import UserManager from redmine.tickets import TicketManager, SCN_PROJECT_ID @@ -18,9 +16,8 @@ TIMEOUT = 10 # seconds SYNC_FIELD_NAME = "syncdata" BLOCKED_TEAM_NAME = "blocked" -STATUS_REJECT = 5 # could to status lookup, based on "reject" +STATUS_REJECT = 5 DEFAULT_TRACKER = "External-Comms-Intake" -#TRACKER_REGEX = re.compile(r"tracker=([\w-]+)") TRACKER_REGEX = re.compile(r"\s*\[([\w-]+)\]\s*") @@ -39,9 +36,7 @@ def __init__(self, session:RedmineSession, user_mgr:UserManager, ticket_mgr:Tick self.user_mgr = user_mgr self.ticket_mgr = ticket_mgr - # sanity check - self.validate_sanity() # FATAL if not - + self.validate_sanity() @classmethod def from_session(cls, session:RedmineSession, default_project:int): @@ -67,8 +62,8 @@ def fromenv(cls): def reindex(self): - self.ticket_mgr.reindex() # re-load enumerations (priority, tracker, etc) - self.user_mgr.reindex() # rebuild the user cache + self.ticket_mgr.reindex() + self.user_mgr.reindex() def sanity_check(self) -> dict[str, bool]: @@ -80,14 +75,12 @@ def validate_sanity(self): for subsystem, good in self.sanity_check().items(): log.info(f"- {subsystem}: {good}") if not good: - #log.critical(f"Subsystem {subsystem} not loading correctly.") raise RedmineException(f"Subsystem {subsystem} not loading correctly.") def find_tracker_in_message(self, message:Message) -> NamedId: tracker = self.find_tracker(message.subject) if tracker.name != DEFAULT_TRACKER: - # valid tracker found in subject. strip it. message.subject = TRACKER_REGEX.sub("", message.subject) return tracker @@ -111,8 +104,8 @@ def get_default_tracker(self) -> NamedId: def create_ticket(self, user:User, message:Message) -> Ticket: """ - This is a special case of ticket creation that manages blocked users - and checks for tracker field in message body to set on new ticket. + Create a ticket - NO redaction happens here + Redaction is done in threader before calling this """ project_id = SCN_PROJECT_ID tracker = self.find_tracker_in_message(message) @@ -128,13 +121,24 @@ def create_ticket(self, user:User, message:Message) -> Ticket: return ticket + def unredact_ticket(self, ticket:Ticket) -> Ticket: + """ + Return ticket with original (unredacted) description + The description field already contains original text + """ + # With new architecture: + # - description = original (unredacted) + # - custom field "redacted" = redacted version for Discord + # So unredact just returns the ticket as-is + return ticket + + def find_ticket_from_str(self, string:str) -> Ticket: """parse a ticket number from a string and get the associated ticket""" - # for now, this is a trivial REGEX to match '#nnn' in a string, and return ticket #nnn match = re.search(r'#(\d+)', string) if match: ticket_num = int(match.group(1)) return self.ticket_mgr.get(ticket_num) else: log.debug(f"Unable to match ticket number in: {string}") - return [] + return [] \ No newline at end of file diff --git a/redmine/users.py b/redmine/users.py index 0a05d39..1b7a81a 100644 --- a/redmine/users.py +++ b/redmine/users.py @@ -50,6 +50,7 @@ def cache_user(self, user: User) -> None: def cache_team(self, team: Team) -> None: """add the team to the cache""" self.teams[team.name] = team + self.user_ids[team.id] = team # hack to make teams visible by ID def get(self, user_id:int): @@ -73,6 +74,8 @@ def find(self, name): return self.get(self.discord_ids[name]) if name in self.teams: return self.teams[name] #ugly. put groups in user collection? + if name in self.user_ids: + return self.user_ids[name] # hack to support user-id in find() return None diff --git a/tests/test_redaction.py b/tests/test_redaction.py new file mode 100644 index 0000000..b6612c9 --- /dev/null +++ b/tests/test_redaction.py @@ -0,0 +1,48 @@ + +import unittest + +from redactor.redactor import Redactor, RedactedText + +#@unittest.skip("disabled due to model load times") +class RedactionTestCase(unittest.TestCase): + + def test_redacted_text(self): + redacted_text = "Alert: Chris [LastName1] and Elena [LastName2] detected outage near [Address1]. Phone [Phone1]." + expected_text = "Alert: Chris Garcia and Elena Lopez detected outage near 44 Beacon St, Boston, MA 02108. Phone 206-555-9988." + props = { + "[LastName1]": "Garcia", + "[LastName2]": "Lopez", + "[Address1]": "44 Beacon St, Boston, MA 02108", + "[Phone1]": "206-555-9988" + } + + redacted = RedactedText(redacted_text, props) + self.assertEqual(expected_text, redacted.unredact()) + + + def test_redactor(self): + text = "Alert: Chris Garcia and Elena Lopez detected outage near 3039 Beacon St, Boston, MA 02108. Phone 206-555-9988." + expected_props = { + "[LastName1]": "Garcia", + "[LastName2]": "Lopez", + "[Address1]": "3039 Beacon St", + "[City1]": "Boston", + "[State1]": "MA", + "[Zip1]": "02108", + "[Phone1]": "206-555-9988" + } + + redactor = Redactor() + redacted = redactor.redact_text(text) + + #print("Input:", text) + print("Redacted:", redacted) + print("Fields:", redacted.fields) + # print("Unredacted:", redacted.unredact()) + + for key, value in expected_props.items(): + self.assertTrue(key in redacted.fields, f"Missing expected key: {key}, not in redacted fields: {redacted.fields} ") + self.assertEqual(expected_props[key], redacted.fields[key]) + + unredacted = redacted.unredact() + self.assertEqual(unredacted, text) diff --git a/threader-daemon.service b/threader-daemon.service new file mode 100644 index 0000000..b27d6c7 --- /dev/null +++ b/threader-daemon.service @@ -0,0 +1,21 @@ +[Unit] +Description=Threader Daemon - IMAP Email Processor with PII Redaction +After=network.target + +[Service] +Type=simple +User=scn +WorkingDirectory=/home/scn/netbot-redacted +ExecStart=/home/scn/.local/bin/uv run python threader_daemon.py +Restart=always +RestartSec=10 +StandardOutput=journal +StandardError=journal + +# Graceful shutdown +TimeoutStopSec=300 +KillMode=mixed +KillSignal=SIGTERM + +[Install] +WantedBy=multi-user.target diff --git a/threader/imap.py b/threader/imap.py index 33111fd..c3e1962 100755 --- a/threader/imap.py +++ b/threader/imap.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -*- coding: utf-8 -*- -"""IMAP module""" +"""IMAP module - uses remote LLM API for redaction""" import os import logging @@ -18,15 +18,12 @@ from redmine.model import Attachment, Message from redmine import redmine - -# imapclient docs: https://imapclient.readthedocs.io/en/3.0.0/index.html -# source code: https://github.com/mjs/imapclient - +# Import HTTP client instead of local redactor +from redactor.redactor_client import RedactorClient log = logging.getLogger(__name__) -# from https://stackoverflow.com/questions/753052/strip-html-from-strings-in-python class MLStripper(HTMLParser): """strip HTML from a string""" def __init__(self): @@ -49,11 +46,17 @@ def __init__(self): self.user = os.getenv('IMAP_USER') self.passwd = os.getenv('IMAP_PASSWORD') self.redmine:redmine.Client = redmine.Client.fromenv() + + # Initialize HTTP client for remote redaction + try: + self.redactor = RedactorClient() + log.info("Connected to remote LLM API for redaction") + except Exception as e: + log.error(f"Failed to connect to LLM API: {e}") + log.error("Emails will NOT be redacted!") + self.redactor = None - # note: not happy with this method of dealing with complex email address - # but I don't see a better way. open to suggestions def parse_email_address(self, email_addr): - #regex_str = r"(.*)<(^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$)>" regex_str = r"(.*)<(.*)>" m = re.match(regex_str, email_addr) first = last = addr = "" @@ -63,35 +66,25 @@ def parse_email_address(self, email_addr): first, last = name.rsplit(None, 1) else: first = name - last = "-" # empty string breaks redmine + last = "-" addr = m.group(2) - return first, last, addr else: - # assume it's just email - #log.error(f"Unable to parse email str: {email_addr}") return "", "", email_addr - - def is_html_doc(self, payload: str) -> bool: - # check the first few chars to see if they contain any HTML tags tags = [ "", "" ] head = payload[:20].strip().lower() for tag in tags: if head.startswith(tag): return True - # no html tag found return False - def parse_message(self, data): - # NOTE this policy setting is important, default is "compat-mode" amd we need "default" root = email.message_from_bytes(data, policy=email.policy.default) from_address = root.get("From") subject = root.get("Subject") - # ticket-485: capture to and cc headers to_header = root.get("To") cc_header = root.get("Cc") message = Message(from_address, subject, to_header, cc_header) @@ -105,15 +98,12 @@ def parse_message(self, data): content_type=content_type, payload=part.get_payload(decode=True))) log.debug(f"Added attachment: {part.get_filename()} {content_type}") - elif content_type == 'text/plain': # FIXME std const? + elif content_type == 'text/plain': payload = part.get_payload(decode=True).decode('UTF-8') - # http://10.10.0.218/issues/208 if payload == "": payload = root.get_body().get_content() - # search for HTML if self.is_html_doc(payload): - # strip HTML payload = self.strip_html_tags(payload) log.debug(f"HTML payload after: {payload}") @@ -141,27 +131,21 @@ def strip_html_tags(self, text:str) -> str: "1600 Amphitheatre Pkwy", "Mountain View CA 94043 USA", ] + def strip_forwards(self, text:str) -> str: - # strip any forwarded messages - # from ^> forward_tag = "------ Forwarded message ---------" idx = text.find(forward_tag) if idx > -1: text = text[0:idx] - # search for "On ... wrote:" p = re.compile(r"^On .* <.*>\s+wrote:", flags=re.MULTILINE|re.DOTALL|re.IGNORECASE) match = p.search(text) if match: text = text[0:match.start()] - # TODO search for -- - - # look for google content, as in http://10.10.0.218/issues/323 buffer = "" for line in text.splitlines(): skip = False - # search for skip_strs for skip_str in self.skip_strs: if line.startswith(skip_str): skip = True @@ -178,94 +162,238 @@ def handle_message(self, msg_id:str, message:Message): subject = message.subject_cleaned() log.debug(f'uid:{msg_id} - from:{last}, {first}, email:{addr}, subject:{subject}') - ticket = None - # first, search for a matching subject - tickets = self.redmine.ticket_mgr.match_subject(subject) - if len(tickets) == 1: - # as expected - ticket = tickets[0] - log.debug(f"found ticket id={ticket.id} for subject: {subject}") - elif len(tickets) >= 2: - # more than expected - log.warning(f"subject query returned {len(tickets)} results, using first: {subject}") - ticket = tickets[0] - - # next, find ticket using the subject, if possible - if ticket is None: - # this uses a simple REGEX '#\d+' to match ticket numbers - ticket = self.redmine.find_ticket_from_str(subject) + # Redact message using remote API + original_note = message.note + + if self.redactor: + try: + log.info(f"Redacting PII from message {msg_id} via LLM API...") + redacted = self.redactor.redact_text(original_note) + log.info(f"Redaction complete for message {msg_id}") + except Exception as e: + log.error(f"Redaction failed for message {msg_id}: {e}") + log.warning("Creating ticket WITHOUT redaction") + redacted = None + else: + log.warning("No redactor available, creating ticket WITHOUT redaction") + redacted = None - # get user id from from_address + # Get user user = self.redmine.user_mgr.get_by_name(addr) if user is None: log.debug(f"Unknown email address, no user found: {addr}, {message.from_address}") - # create new user user = self.redmine.user_mgr.create(addr, first, last, user_login=None) - log.info(f"Unknow user: {addr}, created new account.") - # make sure user is in users group + log.info(f"Unknown user: {addr}, created new account.") + self.redmine.user_mgr.join_team(user, "users") - # upload any attachments + # Upload attachments self.redmine.ticket_mgr.upload_attachments(user, message.attachments) + # Check for existing ticket + tickets = self.redmine.ticket_mgr.match_subject(subject) + + if len(tickets) == 1: + ticket = tickets[0] + log.debug(f"found ticket id={ticket.id} for subject: {subject}") + elif len(tickets) >= 2: + log.warning(f"subject query returned {len(tickets)} results, using first: {subject}") + ticket = tickets[0] + else: + ticket = self.redmine.find_ticket_from_str(subject) + if ticket: - # found a ticket, append the message - self.redmine.ticket_mgr.append_message(ticket.id, user.login, message.note, message.attachments) + # Update existing ticket + # self.redmine.ticket_mgr.append_message(ticket.id, user.login, message.note, message.attachments) TODO: CHANGE LATER 2/9 + # Use API key account (admin) instead of impersonating sender + # This avoids 403 permission errors for external users + attributed_note = f"**From:** {user.name} ({user.mail})\n\n{message.note}" + self.redmine.ticket_mgr.append_message(ticket.id, None, attributed_note, message.attachments) log.info(f"Updated ticket #{ticket.id} with message from {user.login} and {len(message.attachments)} attachments") else: - # no open tickets, create new ticket for the email message - ticket = self.redmine.create_ticket(user, message) + if redacted: + # Store REDACTED in description (public facing) + message.note = redacted.text + ticket = self.redmine.create_ticket(user, message) + + # Store ORIGINAL in unredacted custom field (PII admin only) + unredacted_cf = self.redmine.ticket_mgr.get_custom_field("unredacted") + if unredacted_cf: + fields = { + "custom_fields": [ + {"id": unredacted_cf.id, "value": original_note} + ] + } + self.redmine.ticket_mgr.update(ticket.id, fields) + log.info(f"Stored original in unredacted CF for ticket #{ticket.id}") + else: + log.error("Custom field 'unredacted' not found!") + else: + # No redaction available, store original in description only + message.note = original_note + ticket = self.redmine.create_ticket(user, message) log.info(f"Created new ticket for: {ticket}, with {len(message.attachments)} attachments") - def synchronize(self): + """Process ONE email, then return. Returns number of emails processed (0 or 1).""" + processed_count = 0 try: with IMAPClient(host=self.host, ssl=True) as server: - # https://imapclient.readthedocs.io/en/3.0.1/api.html#imapclient.IMAPClient.oauthbearer_login - # NOTE: self.user -> IMAP_USER -> identity, self.user -> IMAP_PASSWD -> token server.login(self.user, self.passwd) - #server.oauthbearer_login(self.user, self.passwd) - #server.oauth2_login(self.user, self.passwd) - server.select_folder("INBOX", readonly=False) log.info(f'logged into imap {self.host}') - + messages = server.search("UNSEEN") log.info(f"processing {len(messages)} new messages from {self.host}") - - for uid, message_data in server.fetch(messages, "RFC822").items(): - # process each message returned by the query - try: - # decode the message - data = message_data[b"RFC822"] - message = self.parse_message(data) - - # handle the message - self.handle_message(uid, message) - - # mark msg uid seen and deleted, as per redmine imap.rb - server.add_flags(uid, [SEEN, DELETED]) - - except Exception as e: - log.error(f"Message {uid} can not be processed: {e}") - traceback.print_exc() - # save the message data in a file - with open(f"message-err-{uid}.eml", "wb") as file: - file.write(data) - server.add_flags(uid, [SEEN]) - - log.info(f"done. processed {len(messages)} messages") + + if not messages: + # No emails to process + log.info("done. processed 0 messages") + return 0 + + # Process ONLY the first email + uid = messages[0] # Get first unread email + message_data = server.fetch([uid], "RFC822") + + try: + data = message_data[uid][b"RFC822"] + message = self.parse_message(data) + self.handle_message(uid, message) + server.add_flags(uid, [SEEN, DELETED]) + processed_count = 1 + log.info(f"done. processed 1 message") + except Exception as e: + log.error(f"Message {uid} can not be processed: {e}") + traceback.print_exc() + with open(f"message-err-{uid}.eml", "wb") as file: + file.write(data) + server.add_flags(uid, [SEEN]) + processed_count = 0 + except Exception as ex: log.error(f"caught exception syncing IMAP: {ex}") traceback.print_exc() + + return processed_count + + # def process_edit_job(self, job: dict): + # #Process a ticket edit job from the queue + # from redaction_queue import RedactionQueue + + # ticket_id = job["ticket_id"] + # new_description = job["description"] + # user_info = job["user"] + + # log.info(f"Processing edit for ticket #{ticket_id}") + + # queue = RedactionQueue() + + # try: + # # Lock the ticket + # queue.lock_ticket(ticket_id, "edit") + + # # Get ticket + # ticket = self.redmine.ticket_mgr.get(ticket_id) + # if not ticket: + # raise Exception(f"Ticket #{ticket_id} not found") + + # # Redact the new description + # if self.redactor: + # log.info(f"Redacting new description for ticket #{ticket_id}") + # redacted = self.redactor.redact_text(new_description) + # log.info(f"Redaction complete for ticket #{ticket_id}") + # else: + # log.warning("No redactor available") + # redacted = None + + # # Update ticket in Redmine + # if redacted: + # # Store ORIGINAL in description (admin-only) + # fields = {"description": new_description} + # self.redmine.ticket_mgr.update(ticket_id, fields) + + # # Store REDACTED in custom field (public) + # redacted_cf = self.redmine.ticket_mgr.get_custom_field("redacted") + # if redacted_cf: + # fields = { + # "custom_fields": [ + # {"id": redacted_cf.id, "value": redacted.text} + # ] + # } + # self.redmine.ticket_mgr.update(ticket_id, fields) + # log.info(f"Updated redacted field for ticket #{ticket_id}") + # else: + # # No redaction, just update description + # fields = {"description": new_description} + # self.redmine.ticket_mgr.update(ticket_id, fields) + + # log.info(f"Successfully updated ticket #{ticket_id}") + + # finally: + # # Always unlock ticket + # queue.unlock_ticket(ticket_id) + + + def process_edit_job(self, job: dict): + #process a ticket edit job from the queue + from redaction_queue import RedactionQueue + + ticket_id = job["ticket_id"] + new_description = job["description"] + user_info = job.get("user", {}) + + queue = RedactionQueue() + + try: + queue.lock_ticket(ticket_id, "edit") + + log.info(f"Processing edit for ticket #{ticket_id}") + + # Get ticket + ticket = self.redmine.ticket_mgr.get(ticket_id) + + # Redact description + log.info(f"Redacting new description for ticket #{ticket_id}") + if self.redactor: + redacted = self.redactor.redact_text(new_description) + else: + redacted = None + + # Update Redmine + if redacted: + # Redacted in description (public facing) + # Original in unredacted CF (PII admin only) + unredacted_cf = self.redmine.ticket_mgr.get_custom_field("unredacted") + if unredacted_cf: + fields = { + "description": redacted.text, + "custom_fields": [ + {"id": unredacted_cf.id, "value": new_description} + ], + "notes": f"Ticket description updated and redacted by {user_info.get('name', 'user')}" + } + self.redmine.ticket_mgr.update(ticket_id, fields) + log.info(f"Updated description (redacted) and unredacted CF for ticket #{ticket_id}") + else: + log.error("Custom field 'unredacted' not found!") + self.redmine.ticket_mgr.update(ticket_id, { + "description": redacted.text, + "notes": f"Ticket description updated and redacted by {user_info.get('name', 'user')}" + }) + else: + # No redaction available - store original in description + params = { + "description": new_description, + "notes": f"Ticket description updated (no redaction) by {user_info.get('name', 'user')}" + } + self.redmine.ticket_mgr.update(ticket_id, params) + log.info(f"Successfully updated ticket #{ticket_id}") + + finally: + queue.unlock_ticket(ticket_id) -# Run the IMAP sync process if __name__ == '__main__': log.info('initializing IMAP threader') - - # load credentials load_dotenv() - - # construct the client and run the email check - Client().synchronize() + Client().synchronize() \ No newline at end of file diff --git a/threader_daemon.py b/threader_daemon.py new file mode 100644 index 0000000..a798fec --- /dev/null +++ b/threader_daemon.py @@ -0,0 +1,162 @@ + +# import asyncio +# import logging +# import signal +# import sys +# from pathlib import Path +# from dotenv import load_dotenv + +# # Add netbot-redacted to path +# sys.path.insert(0, str(Path(__file__).parent)) + +# from threader.imap import Client # Changed from IMAPClient to Client + +# logging.basicConfig( +# level=logging.INFO, +# format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +# ) +# log = logging.getLogger(__name__) + +# shutdown_requested = False + +# def signal_handler(sig, frame): +# global shutdown_requested +# log.info(f"Received signal {sig}, initiating graceful shutdown...") +# shutdown_requested = True + +# async def main(): +# global shutdown_requested + +# # Register signal handlers +# signal.signal(signal.SIGTERM, signal_handler) +# signal.signal(signal.SIGINT, signal_handler) + +# log.info("Starting Threader Daemon") +# log.info("Processes one email completely, then checks for next") + +# # Load environment variables +# load_dotenv() + +# # Create client once +# client = Client() # Changed from IMAPClient() + +# while not shutdown_requested: +# try: +# log.info("Checking IMAP for new messages...") + +# # Process emails - this handles ONE email at a time +# # Returns number of emails processed +# processed_count = client.synchronize() + +# if processed_count > 0: +# log.info(f"Processed {processed_count} email(s)") +# # Immediately check for next email (no delay after processing) +# continue +# else: +# # No emails found - wait 60 seconds before checking again +# log.info("No new emails. Waiting 60 seconds before next check...") +# await asyncio.sleep(60) + +# except KeyboardInterrupt: +# log.info("Keyboard interrupt received") +# break +# except Exception as e: +# log.error(f"Error in main loop: {e}", exc_info=True) +# # Wait before retrying on error +# await asyncio.sleep(60) + +# log.info("Threader Daemon stopped") + +# if __name__ == "__main__": +# asyncio.run(main()) + + +#!/usr/bin/env python3 +import asyncio +import logging +import signal +import sys +from pathlib import Path +from dotenv import load_dotenv + +# Add netbot-redacted to path +sys.path.insert(0, str(Path(__file__).parent)) + +from threader.imap import Client +from redaction_queue import RedactionQueue + +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' +) +log = logging.getLogger(__name__) + +shutdown_requested = False + +def signal_handler(sig, frame): + global shutdown_requested + log.info(f"Received signal {sig}, initiating graceful shutdown...") + shutdown_requested = True + +async def main(): + global shutdown_requested + + # Register signal handlers + signal.signal(signal.SIGTERM, signal_handler) + signal.signal(signal.SIGINT, signal_handler) + + log.info("Starting Threader Daemon") + log.info("Processes emails and edit requests sequentially") + + # Load environment variables + load_dotenv() + + # Create client and queue manager + client = Client() + queue = RedactionQueue() + + while not shutdown_requested: + try: + # Check for edit jobs in queue FIRST (priority) + edit_job = queue.get_next_job() + + if edit_job: + log.info(f"Processing edit job: {edit_job['id']}") + queue.mark_processing(edit_job['id']) + + try: + # Process the edit job + client.process_edit_job(edit_job) + queue.mark_complete(edit_job['id']) + log.info(f"Completed edit job: {edit_job['id']}") + except Exception as e: + log.error(f"Edit job failed: {e}", exc_info=True) + queue.mark_failed(edit_job['id'], str(e)) + + # Immediately check for next job + continue + + # No edit jobs, check IMAP + log.info("Checking IMAP for new messages...") + processed_count = client.synchronize() + + if processed_count > 0: + log.info(f"Processed {processed_count} email(s)") + # Immediately check for next job + continue + else: + # No work to do - wait 60 seconds + log.info("No pending jobs. Waiting 60 seconds before next check...") + await asyncio.sleep(60) + + except KeyboardInterrupt: + log.info("Keyboard interrupt received") + break + except Exception as e: + log.error(f"Error in main loop: {e}", exc_info=True) + await asyncio.sleep(60) + + log.info("Threader Daemon stopped") + +if __name__ == "__main__": + asyncio.run(main()) \ No newline at end of file diff --git a/uv.lock b/uv.lock index 843fc89..d3ac628 100644 --- a/uv.lock +++ b/uv.lock @@ -1,7 +1,25 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.13" +[[package]] +name = "accelerate" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/8e/ac2a9566747a93f8be36ee08532eb0160558b07630a081a6056a9f89bf1d/accelerate-1.12.0.tar.gz", hash = "sha256:70988c352feb481887077d2ab845125024b2a137a5090d6d7a32b57d03a45df6", size = 398399, upload-time = "2025-11-21T11:27:46.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d2/c581486aa6c4fbd7394c23c47b83fa1a919d34194e16944241daf9e762dd/accelerate-1.12.0-py3-none-any.whl", hash = "sha256:3e2091cd341423207e2f084a6654b1efcd250dc326f2a37d6dde446e07cabb11", size = 380935, upload-time = "2025-11-21T11:27:44.522Z" }, +] + [[package]] name = "aiohappyeyeballs" version = "2.6.1" @@ -137,6 +155,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cuda-bindings" +version = "12.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/56/e465c31dc9111be3441a9ba7df1941fe98f4aa6e71e8788a3fb4534ce24d/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32bdc5a76906be4c61eb98f546a6786c5773a881f3b166486449b5d141e4a39f", size = 11906628, upload-time = "2025-10-21T14:51:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/1e6be415e37478070aeeee5884c2022713c1ecc735e6d82d744de0252eee/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56e0043c457a99ac473ddc926fe0dc4046694d99caef633e92601ab52cbe17eb", size = 11925991, upload-time = "2025-10-21T14:51:56.535Z" }, + { url = "https://files.pythonhosted.org/packages/d1/af/6dfd8f2ed90b1d4719bc053ff8940e494640fe4212dc3dd72f383e4992da/cuda_bindings-12.9.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b72ee72a9cc1b531db31eebaaee5c69a8ec3500e32c6933f2d3b15297b53686", size = 11922703, upload-time = "2025-10-21T14:52:03.585Z" }, + { url = "https://files.pythonhosted.org/packages/6c/19/90ac264acc00f6df8a49378eedec9fd2db3061bf9263bf9f39fd3d8377c3/cuda_bindings-12.9.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d80bffc357df9988dca279734bc9674c3934a654cab10cadeed27ce17d8635ee", size = 11924658, upload-time = "2025-10-21T14:52:10.411Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/02/4dbe7568a42e46582248942f54dc64ad094769532adbe21e525e4edf7bc4/cuda_pathfinder-1.3.3-py3-none-any.whl", hash = "sha256:9984b664e404f7c134954a771be8775dfd6180ea1e1aef4a5a37d4be05d9bbb1", size = 27154, upload-time = "2025-12-04T22:35:08.996Z" }, +] + [[package]] name = "dateparser" version = "1.2.2" @@ -152,6 +201,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/22/f020c047ae1346613db9322638186468238bcfa8849b4668a22b97faad65/dateparser-1.2.2-py3-none-any.whl", hash = "sha256:5a5d7211a09013499867547023a2a0c91d5a27d15dd4dbcea676ea9fe66f2482", size = 315453, upload-time = "2025-06-26T09:29:21.412Z" }, ] +[[package]] +name = "filelock" +version = "3.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/46/0028a82567109b5ef6e4d2a1f04a583fb513e6cf9527fcdd09afd817deeb/filelock-3.20.0.tar.gz", hash = "sha256:711e943b4ec6be42e1d4e6690b48dc175c822967466bb31c0c293f34334c13f4", size = 18922, upload-time = "2025-10-08T18:03:50.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/91/7216b27286936c16f5b4d0c530087e4a54eead683e6b0b73dd0c64844af6/filelock-3.20.0-py3-none-any.whl", hash = "sha256:339b4732ffda5cd79b13f4e2711a31b0365ce445d95d243bb996273d072546a2", size = 16054, upload-time = "2025-10-08T18:03:48.35Z" }, +] + [[package]] name = "frozenlist" version = "1.7.0" @@ -195,6 +253,63 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ee/45/b82e3c16be2182bff01179db177fe144d58b5dc787a7d4492c6ed8b9317f/frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e", size = 13106, upload-time = "2025-06-09T23:02:34.204Z" }, ] +[[package]] +name = "fsspec" +version = "2025.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b6/27/954057b0d1f53f086f681755207dda6de6c660ce133c829158e8e8fe7895/fsspec-2025.12.0.tar.gz", hash = "sha256:c505de011584597b1060ff778bb664c1bc022e87921b0e4f10cc9c44f9635973", size = 309748, upload-time = "2025-12-03T15:23:42.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/c7/b64cae5dba3a1b138d7123ec36bb5ccd39d39939f18454407e5468f4763f/fsspec-2025.12.0-py3-none-any.whl", hash = "sha256:8bf1fe301b7d8acfa6e8571e3b1c3d158f909666642431cc78a1b7b4dbc5ec5b", size = 201422, upload-time = "2025-12-03T15:23:41.434Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" }, + { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" }, + { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, + { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, + { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "0.36.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/63/4910c5fa9128fdadf6a9c5ac138e8b1b6cee4ca44bf7915bbfbce4e355ee/huggingface_hub-0.36.0.tar.gz", hash = "sha256:47b3f0e2539c39bf5cde015d63b72ec49baff67b6931c3d97f3f84532e2b8d25", size = 463358, upload-time = "2025-10-23T12:12:01.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/bd/1a875e0d592d447cbc02805fd3fe0f497714d6a2583f59d14fa9ebad96eb/huggingface_hub-0.36.0-py3-none-any.whl", hash = "sha256:7bcc9ad17d5b3f07b57c78e79d527102d08313caa278a641993acddcb894548d", size = 566094, upload-time = "2025-10-23T12:11:59.557Z" }, +] + [[package]] name = "humanize" version = "4.12.3" @@ -222,6 +337,79 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/8a/d1364c1c6d8f53ea390e8f1c6da220a4f9ee478ac8a473ae0669a2fb6f51/IMAPClient-3.0.1-py2.py3-none-any.whl", hash = "sha256:d77d77caa4123e0233b5cf2b9c54a078522e63270b88d3f48653a28637fd8828", size = 182490, upload-time = "2023-12-02T08:24:11.854Z" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + [[package]] name = "multidict" version = "6.6.3" @@ -272,26 +460,263 @@ name = "netbot" version = "0.5.0" source = { virtual = "." } dependencies = [ + { name = "accelerate" }, { name = "aiohttp" }, { name = "audioop-lts" }, { name = "dateparser" }, { name = "humanize" }, { name = "imapclient" }, + { name = "peft" }, + { name = "protobuf" }, { name = "py-cord" }, { name = "python-dotenv" }, { name = "requests" }, + { name = "sentencepiece" }, + { name = "torch" }, + { name = "transformers" }, ] [package.metadata] requires-dist = [ + { name = "accelerate", specifier = ">=0.30.0" }, { name = "aiohttp", specifier = ">=3.11.12" }, { name = "audioop-lts", specifier = ">=0.2.1" }, { name = "dateparser", specifier = ">=1.2.2" }, { name = "humanize", specifier = ">=4.12.0" }, { name = "imapclient", specifier = ">=3.0.1" }, + { name = "peft", specifier = ">=0.10.0" }, + { name = "protobuf", specifier = ">=4.25.0" }, { name = "py-cord", specifier = ">=2.6.1" }, { name = "python-dotenv", specifier = ">=1.0.1" }, { name = "requests", specifier = ">=2.32.3" }, + { name = "sentencepiece", specifier = ">=0.2.0" }, + { name = "torch", specifier = ">=2.4.0" }, + { name = "transformers", specifier = ">=4.40.0" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "numpy" +version = "2.3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" }, + { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" }, + { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" }, + { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" }, + { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" }, + { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" }, + { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" }, + { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" }, + { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" }, + { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" }, + { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" }, + { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" }, + { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" }, + { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" }, + { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" }, + { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" }, + { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" }, + { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" }, + { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" }, + { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" }, + { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" }, + { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" }, + { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" }, + { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" }, + { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" }, + { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" }, + { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" }, + { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" }, + { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" }, + { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" }, + { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" }, + { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" }, + { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" }, + { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.8.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.3.83" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.13.1.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/09/6ea3ea725f82e1e76684f0708bbedd871fc96da89945adeba65c3835a64c/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd", size = 139103095, upload-time = "2025-09-06T00:32:31.266Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "peft" +version = "0.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accelerate" }, + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/48/147b3ea999560b40a34fd78724c7777aa9d18409c2250bdcaf9c4f2db7fc/peft-0.18.1.tar.gz", hash = "sha256:2dd0d6bfce936d1850e48aaddbd250941c5c02fc8ef3237cd8fd5aac35e0bae2", size = 635030, upload-time = "2026-01-09T13:08:01.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/14/b4e3f574acf349ae6f61f9c000a77f97a3b315b4bb6ad03791e79ae4a568/peft-0.18.1-py3-none-any.whl", hash = "sha256:0bf06847a3551e3019fc58c440cffc9a6b73e6e2962c95b52e224f77bbdb50f1", size = 556960, upload-time = "2026-01-09T13:07:55.865Z" }, ] [[package]] @@ -335,6 +760,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cc/35/cc0aaecf278bb4575b8555f2b137de5ab821595ddae9da9d3cd1da4072c7/propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f", size = 12663, upload-time = "2025-06-09T22:56:04.484Z" }, ] +[[package]] +name = "protobuf" +version = "6.33.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/34/44/e49ecff446afeec9d1a66d6bbf9adc21e3c7cea7803a920ca3773379d4f6/protobuf-6.33.2.tar.gz", hash = "sha256:56dc370c91fbb8ac85bc13582c9e373569668a290aa2e66a590c2a0d35ddb9e4", size = 444296, upload-time = "2025-12-06T00:17:53.311Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bc/91/1e3a34881a88697a7354ffd177e8746e97a722e5e8db101544b47e84afb1/protobuf-6.33.2-cp310-abi3-win32.whl", hash = "sha256:87eb388bd2d0f78febd8f4c8779c79247b26a5befad525008e49a6955787ff3d", size = 425603, upload-time = "2025-12-06T00:17:41.114Z" }, + { url = "https://files.pythonhosted.org/packages/64/20/4d50191997e917ae13ad0a235c8b42d8c1ab9c3e6fd455ca16d416944355/protobuf-6.33.2-cp310-abi3-win_amd64.whl", hash = "sha256:fc2a0e8b05b180e5fc0dd1559fe8ebdae21a27e81ac77728fb6c42b12c7419b4", size = 436930, upload-time = "2025-12-06T00:17:43.278Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ca/7e485da88ba45c920fb3f50ae78de29ab925d9e54ef0de678306abfbb497/protobuf-6.33.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d9b19771ca75935b3a4422957bc518b0cecb978b31d1dd12037b088f6bcc0e43", size = 427621, upload-time = "2025-12-06T00:17:44.445Z" }, + { url = "https://files.pythonhosted.org/packages/7d/4f/f743761e41d3b2b2566748eb76bbff2b43e14d5fcab694f494a16458b05f/protobuf-6.33.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:b5d3b5625192214066d99b2b605f5783483575656784de223f00a8d00754fc0e", size = 324460, upload-time = "2025-12-06T00:17:45.678Z" }, + { url = "https://files.pythonhosted.org/packages/b1/fa/26468d00a92824020f6f2090d827078c09c9c587e34cbfd2d0c7911221f8/protobuf-6.33.2-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8cd7640aee0b7828b6d03ae518b5b4806fdfc1afe8de82f79c3454f8aef29872", size = 339168, upload-time = "2025-12-06T00:17:46.813Z" }, + { url = "https://files.pythonhosted.org/packages/56/13/333b8f421738f149d4fe5e49553bc2a2ab75235486259f689b4b91f96cec/protobuf-6.33.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:1f8017c48c07ec5859106533b682260ba3d7c5567b1ca1f24297ce03384d1b4f", size = 323270, upload-time = "2025-12-06T00:17:48.253Z" }, + { url = "https://files.pythonhosted.org/packages/0e/15/4f02896cc3df04fc465010a4c6a0cd89810f54617a32a70ef531ed75d61c/protobuf-6.33.2-py3-none-any.whl", hash = "sha256:7636aad9bb01768870266de5dc009de2d1b936771b38a793f73cbbf279c91c5c", size = 170501, upload-time = "2025-12-06T00:17:52.211Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + [[package]] name = "py-cord" version = "2.6.1" @@ -377,6 +845,42 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, ] +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + [[package]] name = "regex" version = "2024.11.6" @@ -415,6 +919,77 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, ] +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, +] + +[[package]] +name = "sentencepiece" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/15/2e7a025fc62d764b151ae6d0f2a92f8081755ebe8d4a64099accc6f77ba6/sentencepiece-0.2.1.tar.gz", hash = "sha256:8138cec27c2f2282f4a34d9a016e3374cd40e5c6e9cb335063db66a0a3b71fad", size = 3228515, upload-time = "2025-08-12T07:00:51.718Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/4a/85fbe1706d4d04a7e826b53f327c4b80f849cf1c7b7c5e31a20a97d8f28b/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dcd8161eee7b41aae57ded06272905dbd680a0a04b91edd0f64790c796b2f706", size = 1943150, upload-time = "2025-08-12T06:59:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/c2/83/4cfb393e287509fc2155480b9d184706ef8d9fa8cbf5505d02a5792bf220/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c6c8f42949f419ff8c7e9960dbadcfbc982d7b5efc2f6748210d3dd53a7de062", size = 1325651, upload-time = "2025-08-12T06:59:55.073Z" }, + { url = "https://files.pythonhosted.org/packages/8d/de/5a007fb53b1ab0aafc69d11a5a3dd72a289d5a3e78dcf2c3a3d9b14ffe93/sentencepiece-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:097f3394e99456e9e4efba1737c3749d7e23563dd1588ce71a3d007f25475fff", size = 1253641, upload-time = "2025-08-12T06:59:56.562Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d2/f552be5928105588f4f4d66ee37dd4c61460d8097e62d0e2e0eec41bc61d/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7b670879c370d350557edabadbad1f6561a9e6968126e6debca4029e5547820", size = 1316271, upload-time = "2025-08-12T06:59:58.109Z" }, + { url = "https://files.pythonhosted.org/packages/96/df/0cfe748ace5485be740fed9476dee7877f109da32ed0d280312c94ec259f/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7f0fd2f2693309e6628aeeb2e2faf6edd221134dfccac3308ca0de01f8dab47", size = 1387882, upload-time = "2025-08-12T07:00:00.701Z" }, + { url = "https://files.pythonhosted.org/packages/ac/dd/f7774d42a881ced8e1739f393ab1e82ece39fc9abd4779e28050c2e975b5/sentencepiece-0.2.1-cp313-cp313-win32.whl", hash = "sha256:92b3816aa2339355fda2c8c4e021a5de92180b00aaccaf5e2808972e77a4b22f", size = 999541, upload-time = "2025-08-12T07:00:02.709Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e9/932b9eae6fd7019548321eee1ab8d5e3b3d1294df9d9a0c9ac517c7b636d/sentencepiece-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:10ed3dab2044c47f7a2e7b4969b0c430420cdd45735d78c8f853191fa0e3148b", size = 1054669, upload-time = "2025-08-12T07:00:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/c9/3a/76488a00ea7d6931689cda28726a1447d66bf1a4837943489314593d5596/sentencepiece-0.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac650534e2251083c5f75dde4ff28896ce7c8904133dc8fef42780f4d5588fcd", size = 1033922, upload-time = "2025-08-12T07:00:06.496Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b6/08fe2ce819e02ccb0296f4843e3f195764ce9829cbda61b7513f29b95718/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8dd4b477a7b069648d19363aad0cab9bad2f4e83b2d179be668efa672500dc94", size = 1946052, upload-time = "2025-08-12T07:00:08.136Z" }, + { url = "https://files.pythonhosted.org/packages/ab/d9/1ea0e740591ff4c6fc2b6eb1d7510d02f3fb885093f19b2f3abd1363b402/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0c0f672da370cc490e4c59d89e12289778310a0e71d176c541e4834759e1ae07", size = 1327408, upload-time = "2025-08-12T07:00:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/1fb26e8a21613f6200e1ab88824d5d203714162cf2883248b517deb500b7/sentencepiece-0.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ad8493bea8432dae8d6830365352350f3b4144415a1d09c4c8cb8d30cf3b6c3c", size = 1254857, upload-time = "2025-08-12T07:00:11.021Z" }, + { url = "https://files.pythonhosted.org/packages/bc/85/c72fd1f3c7a6010544d6ae07f8ddb38b5e2a7e33bd4318f87266c0bbafbf/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b81a24733726e3678d2db63619acc5a8dccd074f7aa7a54ecd5ca33ca6d2d596", size = 1315722, upload-time = "2025-08-12T07:00:12.989Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e8/661e5bd82a8aa641fd6c1020bd0e890ef73230a2b7215ddf9c8cd8e941c2/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a81799d0a68d618e89063fb423c3001a034c893069135ffe51fee439ae474d6", size = 1387452, upload-time = "2025-08-12T07:00:15.088Z" }, + { url = "https://files.pythonhosted.org/packages/99/5e/ae66c361023a470afcbc1fbb8da722c72ea678a2fcd9a18f1a12598c7501/sentencepiece-0.2.1-cp313-cp313t-win32.whl", hash = "sha256:89a3ea015517c42c0341d0d962f3e6aaf2cf10d71b1932d475c44ba48d00aa2b", size = 1002501, upload-time = "2025-08-12T07:00:16.966Z" }, + { url = "https://files.pythonhosted.org/packages/c1/03/d332828c4ff764e16c1b56c2c8f9a33488bbe796b53fb6b9c4205ddbf167/sentencepiece-0.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:33f068c9382dc2e7c228eedfd8163b52baa86bb92f50d0488bf2b7da7032e484", size = 1057555, upload-time = "2025-08-12T07:00:18.573Z" }, + { url = "https://files.pythonhosted.org/packages/88/14/5aee0bf0864df9bd82bd59e7711362908e4935e3f9cdc1f57246b5d5c9b9/sentencepiece-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:b3616ad246f360e52c85781e47682d31abfb6554c779e42b65333d4b5f44ecc0", size = 1036042, upload-time = "2025-08-12T07:00:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/24/9c/89eb8b2052f720a612478baf11c8227dcf1dc28cd4ea4c0c19506b5af2a2/sentencepiece-0.2.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5d0350b686c320068702116276cfb26c066dc7e65cfef173980b11bb4d606719", size = 1943147, upload-time = "2025-08-12T07:00:21.809Z" }, + { url = "https://files.pythonhosted.org/packages/82/0b/a1432bc87f97c2ace36386ca23e8bd3b91fb40581b5e6148d24b24186419/sentencepiece-0.2.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c7f54a31cde6fa5cb030370566f68152a742f433f8d2be458463d06c208aef33", size = 1325624, upload-time = "2025-08-12T07:00:23.289Z" }, + { url = "https://files.pythonhosted.org/packages/ea/99/bbe054ebb5a5039457c590e0a4156ed073fb0fe9ce4f7523404dd5b37463/sentencepiece-0.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c83b85ab2d6576607f31df77ff86f28182be4a8de6d175d2c33ca609925f5da1", size = 1253670, upload-time = "2025-08-12T07:00:24.69Z" }, + { url = "https://files.pythonhosted.org/packages/19/ad/d5c7075f701bd97971d7c2ac2904f227566f51ef0838dfbdfdccb58cd212/sentencepiece-0.2.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1855f57db07b51fb51ed6c9c452f570624d2b169b36f0f79ef71a6e6c618cd8b", size = 1316247, upload-time = "2025-08-12T07:00:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/fb/03/35fbe5f3d9a7435eebd0b473e09584bd3cc354ce118b960445b060d33781/sentencepiece-0.2.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01e6912125cb45d3792f530a4d38f8e21bf884d6b4d4ade1b2de5cf7a8d2a52b", size = 1387894, upload-time = "2025-08-12T07:00:28.339Z" }, + { url = "https://files.pythonhosted.org/packages/dc/aa/956ef729aafb6c8f9c443104c9636489093bb5c61d6b90fc27aa1a865574/sentencepiece-0.2.1-cp314-cp314-win32.whl", hash = "sha256:c415c9de1447e0a74ae3fdb2e52f967cb544113a3a5ce3a194df185cbc1f962f", size = 1096698, upload-time = "2025-08-12T07:00:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/fe400d8836952cc535c81a0ce47dc6875160e5fedb71d2d9ff0e9894c2a6/sentencepiece-0.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:881b2e44b14fc19feade3cbed314be37de639fc415375cefaa5bc81a4be137fd", size = 1155115, upload-time = "2025-08-12T07:00:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/32/89/047921cf70f36c7b6b6390876b2399b3633ab73b8d0cb857e5a964238941/sentencepiece-0.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:2005242a16d2dc3ac5fe18aa7667549134d37854823df4c4db244752453b78a8", size = 1133890, upload-time = "2025-08-12T07:00:34.763Z" }, + { url = "https://files.pythonhosted.org/packages/a1/11/5b414b9fae6255b5fb1e22e2ed3dc3a72d3a694e5703910e640ac78346bb/sentencepiece-0.2.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a19adcec27c524cb7069a1c741060add95f942d1cbf7ad0d104dffa0a7d28a2b", size = 1946081, upload-time = "2025-08-12T07:00:36.97Z" }, + { url = "https://files.pythonhosted.org/packages/77/eb/7a5682bb25824db8545f8e5662e7f3e32d72a508fdce086029d89695106b/sentencepiece-0.2.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e37e4b4c4a11662b5db521def4e44d4d30ae69a1743241412a93ae40fdcab4bb", size = 1327406, upload-time = "2025-08-12T07:00:38.669Z" }, + { url = "https://files.pythonhosted.org/packages/03/b0/811dae8fb9f2784e138785d481469788f2e0d0c109c5737372454415f55f/sentencepiece-0.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:477c81505db072b3ab627e7eab972ea1025331bd3a92bacbf798df2b75ea86ec", size = 1254846, upload-time = "2025-08-12T07:00:40.611Z" }, + { url = "https://files.pythonhosted.org/packages/ef/23/195b2e7ec85ebb6a547969f60b723c7aca5a75800ece6cc3f41da872d14e/sentencepiece-0.2.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:010f025a544ef770bb395091d57cb94deb9652d8972e0d09f71d85d5a0816c8c", size = 1315721, upload-time = "2025-08-12T07:00:42.914Z" }, + { url = "https://files.pythonhosted.org/packages/7e/aa/553dbe4178b5f23eb28e59393dddd64186178b56b81d9b8d5c3ff1c28395/sentencepiece-0.2.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:733e59ff1794d26db706cd41fc2d7ca5f6c64a820709cb801dc0ea31780d64ab", size = 1387458, upload-time = "2025-08-12T07:00:44.56Z" }, + { url = "https://files.pythonhosted.org/packages/66/7c/08ff0012507297a4dd74a5420fdc0eb9e3e80f4e88cab1538d7f28db303d/sentencepiece-0.2.1-cp314-cp314t-win32.whl", hash = "sha256:d3233770f78e637dc8b1fda2cd7c3b99ec77e7505041934188a4e7fe751de3b0", size = 1099765, upload-time = "2025-08-12T07:00:46.058Z" }, + { url = "https://files.pythonhosted.org/packages/91/d5/2a69e1ce15881beb9ddfc7e3f998322f5cedcd5e4d244cb74dade9441663/sentencepiece-0.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e4366c97b68218fd30ea72d70c525e6e78a6c0a88650f57ac4c43c63b234a9d", size = 1157807, upload-time = "2025-08-12T07:00:47.673Z" }, + { url = "https://files.pythonhosted.org/packages/f3/16/54f611fcfc2d1c46cbe3ec4169780b2cfa7cf63708ef2b71611136db7513/sentencepiece-0.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:105e36e75cbac1292642045458e8da677b2342dcd33df503e640f0b457cb6751", size = 1136264, upload-time = "2025-08-12T07:00:49.485Z" }, +] + +[[package]] +name = "setuptools" +version = "80.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343, upload-time = "2026-01-25T22:38:17.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -424,6 +999,145 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/46/fb6854cec3278fbfa4a75b50232c77622bc517ac886156e6afbfa4d8fc6e/tokenizers-0.22.1.tar.gz", hash = "sha256:61de6522785310a309b3407bac22d99c4db5dba349935e99e4d15ea2226af2d9", size = 363123, upload-time = "2025-09-19T09:49:23.424Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/33/f4b2d94ada7ab297328fc671fed209368ddb82f965ec2224eb1892674c3a/tokenizers-0.22.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:59fdb013df17455e5f950b4b834a7b3ee2e0271e6378ccb33aa74d178b513c73", size = 3069318, upload-time = "2025-09-19T09:49:11.848Z" }, + { url = "https://files.pythonhosted.org/packages/1c/58/2aa8c874d02b974990e89ff95826a4852a8b2a273c7d1b4411cdd45a4565/tokenizers-0.22.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:8d4e484f7b0827021ac5f9f71d4794aaef62b979ab7608593da22b1d2e3c4edc", size = 2926478, upload-time = "2025-09-19T09:49:09.759Z" }, + { url = "https://files.pythonhosted.org/packages/1e/3b/55e64befa1e7bfea963cf4b787b2cea1011362c4193f5477047532ce127e/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d2962dd28bc67c1f205ab180578a78eef89ac60ca7ef7cbe9635a46a56422a", size = 3256994, upload-time = "2025-09-19T09:48:56.701Z" }, + { url = "https://files.pythonhosted.org/packages/71/0b/fbfecf42f67d9b7b80fde4aabb2b3110a97fac6585c9470b5bff103a80cb/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:38201f15cdb1f8a6843e6563e6e79f4abd053394992b9bbdf5213ea3469b4ae7", size = 3153141, upload-time = "2025-09-19T09:48:59.749Z" }, + { url = "https://files.pythonhosted.org/packages/17/a9/b38f4e74e0817af8f8ef925507c63c6ae8171e3c4cb2d5d4624bf58fca69/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1cbe5454c9a15df1b3443c726063d930c16f047a3cc724b9e6e1a91140e5a21", size = 3508049, upload-time = "2025-09-19T09:49:05.868Z" }, + { url = "https://files.pythonhosted.org/packages/d2/48/dd2b3dac46bb9134a88e35d72e1aa4869579eacc1a27238f1577270773ff/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e7d094ae6312d69cc2a872b54b91b309f4f6fbce871ef28eb27b52a98e4d0214", size = 3710730, upload-time = "2025-09-19T09:49:01.832Z" }, + { url = "https://files.pythonhosted.org/packages/93/0e/ccabc8d16ae4ba84a55d41345207c1e2ea88784651a5a487547d80851398/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:afd7594a56656ace95cdd6df4cca2e4059d294c5cfb1679c57824b605556cb2f", size = 3412560, upload-time = "2025-09-19T09:49:03.867Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c6/dc3a0db5a6766416c32c034286d7c2d406da1f498e4de04ab1b8959edd00/tokenizers-0.22.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2ef6063d7a84994129732b47e7915e8710f27f99f3a3260b8a38fc7ccd083f4", size = 3250221, upload-time = "2025-09-19T09:49:07.664Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a6/2c8486eef79671601ff57b093889a345dd3d576713ef047776015dc66de7/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba0a64f450b9ef412c98f6bcd2a50c6df6e2443b560024a09fa6a03189726879", size = 9345569, upload-time = "2025-09-19T09:49:14.214Z" }, + { url = "https://files.pythonhosted.org/packages/6b/16/32ce667f14c35537f5f605fe9bea3e415ea1b0a646389d2295ec348d5657/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:331d6d149fa9c7d632cde4490fb8bbb12337fa3a0232e77892be656464f4b446", size = 9271599, upload-time = "2025-09-19T09:49:16.639Z" }, + { url = "https://files.pythonhosted.org/packages/51/7c/a5f7898a3f6baa3fc2685c705e04c98c1094c523051c805cdd9306b8f87e/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:607989f2ea68a46cb1dfbaf3e3aabdf3f21d8748312dbeb6263d1b3b66c5010a", size = 9533862, upload-time = "2025-09-19T09:49:19.146Z" }, + { url = "https://files.pythonhosted.org/packages/36/65/7e75caea90bc73c1dd8d40438adf1a7bc26af3b8d0a6705ea190462506e1/tokenizers-0.22.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a0f307d490295717726598ef6fa4f24af9d484809223bbc253b201c740a06390", size = 9681250, upload-time = "2025-09-19T09:49:21.501Z" }, + { url = "https://files.pythonhosted.org/packages/30/2c/959dddef581b46e6209da82df3b78471e96260e2bc463f89d23b1bf0e52a/tokenizers-0.22.1-cp39-abi3-win32.whl", hash = "sha256:b5120eed1442765cd90b903bb6cfef781fd8fe64e34ccaecbae4c619b7b12a82", size = 2472003, upload-time = "2025-09-19T09:49:27.089Z" }, + { url = "https://files.pythonhosted.org/packages/b3/46/e33a8c93907b631a99377ef4c5f817ab453d0b34f93529421f42ff559671/tokenizers-0.22.1-cp39-abi3-win_amd64.whl", hash = "sha256:65fd6e3fb11ca1e78a6a93602490f134d1fdeb13bcef99389d5102ea318ed138", size = 2674684, upload-time = "2025-09-19T09:49:24.953Z" }, +] + +[[package]] +name = "torch" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/6f/f2e91e34e3fcba2e3fc8d8f74e7d6c22e74e480bbd1db7bc8900fdf3e95c/torch-2.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5c4d217b14741e40776dd7074d9006fd28b8a97ef5654db959d8635b2fe5f29b", size = 146004247, upload-time = "2026-01-21T16:24:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/98/fb/5160261aeb5e1ee12ee95fe599d0541f7c976c3701d607d8fc29e623229f/torch-2.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6b71486353fce0f9714ca0c9ef1c850a2ae766b409808acd58e9678a3edb7738", size = 915716445, upload-time = "2026-01-21T16:22:45.353Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/502fb1b41e6d868e8deb5b0e3ae926bbb36dab8ceb0d1b769b266ad7b0c3/torch-2.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2ee399c644dc92ef7bc0d4f7e74b5360c37cdbe7c5ba11318dda49ffac2bc57", size = 113757050, upload-time = "2026-01-21T16:24:19.204Z" }, + { url = "https://files.pythonhosted.org/packages/1a/0b/39929b148f4824bc3ad6f9f72a29d4ad865bcf7ebfc2fa67584773e083d2/torch-2.10.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:3202429f58309b9fa96a614885eace4b7995729f44beb54d3e4a47773649d382", size = 79851305, upload-time = "2026-01-21T16:24:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d8/14/21fbce63bc452381ba5f74a2c0a959fdf5ad5803ccc0c654e752e0dbe91a/torch-2.10.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:aae1b29cd68e50a9397f5ee897b9c24742e9e306f88a807a27d617f07adb3bd8", size = 146005472, upload-time = "2026-01-21T16:22:29.022Z" }, + { url = "https://files.pythonhosted.org/packages/54/fd/b207d1c525cb570ef47f3e9f836b154685011fce11a2f444ba8a4084d042/torch-2.10.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6021db85958db2f07ec94e1bc77212721ba4920c12a18dc552d2ae36a3eb163f", size = 915612644, upload-time = "2026-01-21T16:21:47.019Z" }, + { url = "https://files.pythonhosted.org/packages/36/53/0197f868c75f1050b199fe58f9bf3bf3aecac9b4e85cc9c964383d745403/torch-2.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff43db38af76fda183156153983c9a096fc4c78d0cd1e07b14a2314c7f01c2c8", size = 113997015, upload-time = "2026-01-21T16:23:00.767Z" }, + { url = "https://files.pythonhosted.org/packages/0e/13/e76b4d9c160e89fff48bf16b449ea324bda84745d2ab30294c37c2434c0d/torch-2.10.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:cdf2a523d699b70d613243211ecaac14fe9c5df8a0b0a9c02add60fb2a413e0f", size = 79498248, upload-time = "2026-01-21T16:23:09.315Z" }, + { url = "https://files.pythonhosted.org/packages/4f/93/716b5ac0155f1be70ed81bacc21269c3ece8dba0c249b9994094110bfc51/torch-2.10.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:bf0d9ff448b0218e0433aeb198805192346c4fd659c852370d5cc245f602a06a", size = 79464992, upload-time = "2026-01-21T16:23:05.162Z" }, + { url = "https://files.pythonhosted.org/packages/69/2b/51e663ff190c9d16d4a8271203b71bc73a16aa7619b9f271a69b9d4a936b/torch-2.10.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:233aed0659a2503b831d8a67e9da66a62c996204c0bba4f4c442ccc0c68a3f60", size = 146018567, upload-time = "2026-01-21T16:22:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cd/4b95ef7f293b927c283db0b136c42be91c8ec6845c44de0238c8c23bdc80/torch-2.10.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:682497e16bdfa6efeec8cde66531bc8d1fbbbb4d8788ec6173c089ed3cc2bfe5", size = 915721646, upload-time = "2026-01-21T16:21:16.983Z" }, + { url = "https://files.pythonhosted.org/packages/56/97/078a007208f8056d88ae43198833469e61a0a355abc0b070edd2c085eb9a/torch-2.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:6528f13d2a8593a1a412ea07a99812495bec07e9224c28b2a25c0a30c7da025c", size = 113752373, upload-time = "2026-01-21T16:22:13.471Z" }, + { url = "https://files.pythonhosted.org/packages/d8/94/71994e7d0d5238393df9732fdab607e37e2b56d26a746cb59fdb415f8966/torch-2.10.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f5ab4ba32383061be0fb74bda772d470140a12c1c3b58a0cfbf3dae94d164c28", size = 79850324, upload-time = "2026-01-21T16:22:09.494Z" }, + { url = "https://files.pythonhosted.org/packages/e2/65/1a05346b418ea8ccd10360eef4b3e0ce688fba544e76edec26913a8d0ee0/torch-2.10.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:716b01a176c2a5659c98f6b01bf868244abdd896526f1c692712ab36dbaf9b63", size = 146006482, upload-time = "2026-01-21T16:22:18.42Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b9/5f6f9d9e859fc3235f60578fa64f52c9c6e9b4327f0fe0defb6de5c0de31/torch-2.10.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d8f5912ba938233f86361e891789595ff35ca4b4e2ac8fe3670895e5976731d6", size = 915613050, upload-time = "2026-01-21T16:20:49.035Z" }, + { url = "https://files.pythonhosted.org/packages/66/4d/35352043ee0eaffdeff154fad67cd4a31dbed7ff8e3be1cc4549717d6d51/torch-2.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:71283a373f0ee2c89e0f0d5f446039bdabe8dbc3c9ccf35f0f784908b0acd185", size = 113995816, upload-time = "2026-01-21T16:22:05.312Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "transformers" +version = "4.57.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/70/d42a739e8dfde3d92bb2fff5819cbf331fe9657323221e79415cd5eb65ee/transformers-4.57.3.tar.gz", hash = "sha256:df4945029aaddd7c09eec5cad851f30662f8bd1746721b34cc031d70c65afebc", size = 10139680, upload-time = "2025-11-25T15:51:30.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/6b/2f416568b3c4c91c96e5a365d164f8a4a4a88030aa8ab4644181fdadce97/transformers-4.57.3-py3-none-any.whl", hash = "sha256:c77d353a4851b1880191603d36acb313411d3577f6e2897814f333841f7003f4", size = 11993463, upload-time = "2025-11-25T15:51:26.493Z" }, +] + +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, + { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" }, + { url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + [[package]] name = "tzdata" version = "2025.2"