diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 0885eee3..e85d252c 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -155,7 +155,7 @@ jobs: PARSE_MAP: true DECOMPILE: true PARSE: true - CHANGELOGS: false + CHANGELOGS: true CLEANUP: false WIKI_UPLOAD: ${{ needs.get-branch.outputs.branch == 'master' }} STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }} diff --git a/docker-compose.yml b/docker-compose.yml index c4a86a37..041fc554 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,7 @@ services: WIKI_UPLOAD: ${WIKI_UPLOAD} FORCE: ${FORCE} CLEANUP: ${CLEANUP} + DRY_RUN: ${DRY_RUN} VERBOSE: ${VERBOSE} STEAM_USERNAME: ${STEAM_USERNAME} STEAM_PASSWORD: ${STEAM_PASSWORD} diff --git a/input-data/changelogs/changelog_configs.json b/input-data/changelogs/changelog_configs.json index d433e5de..ff16c4b6 100644 --- a/input-data/changelogs/changelog_configs.json +++ b/input-data/changelogs/changelog_configs.json @@ -760,5 +760,45 @@ "date": "2026-05-31", "link": "https://forums.playdeadlock.com/threads/05-22-2026-update.135477/post-263371", "is_hero_lab": false + }, + "2026-06-04": { + "source_id": "1834602721188293", + "date": "2026-06-04", + "link": "https://steamstore-a.akamaihd.net/news/externalpost/steam_community_announcements/1834602721188293", + "is_hero_lab": false, + "title": "Minor Update - 06-04-2026", + "steam_hash": "041d77eede2596e9445f6376c1f0a200" + }, + "2026-06-11": { + "source_id": "1835236783562074", + "date": "2026-06-11", + "link": "https://steamstore-a.akamaihd.net/news/externalpost/steam_community_announcements/1835236783562074", + "is_hero_lab": false, + "title": "Minor Update - 06-11-2026", + "steam_hash": "13544b2f29881ee0c7e9748c08831276" + }, + "2026-06-30": { + "source_id": "1836506165563227", + "date": "2026-06-30", + "link": "https://steamstore-a.akamaihd.net/news/externalpost/steam_community_announcements/1836506165563227", + "is_hero_lab": false, + "title": "Minor Update - 06-30-2026", + "steam_hash": "1f5c55a3db4f6a97dd2e045da399bb65" + }, + "2026-07-01": { + "source_id": "1836506165566600", + "date": "2026-07-01", + "link": "https://steamstore-a.akamaihd.net/news/externalpost/steam_community_announcements/1836506165566600", + "is_hero_lab": false, + "title": "Minor Update - 07-01-2026", + "steam_hash": "82ac2c2a5a6746e2b783104cb233396e" + }, + "2026-07-09": { + "source_id": "1836506165584438", + "date": "2026-07-09", + "link": "https://steamstore-a.akamaihd.net/news/externalpost/steam_community_announcements/1836506165584438", + "is_hero_lab": false, + "title": "Minor Update - 07-09-2026", + "steam_hash": "6fb213364297c3a2b480be292400852f" } } \ No newline at end of file diff --git a/input-data/changelogs/raw/2026-06-04.txt b/input-data/changelogs/raw/2026-06-04.txt new file mode 100644 index 00000000..3a89127d --- /dev/null +++ b/input-data/changelogs/raw/2026-06-04.txt @@ -0,0 +1,18 @@ +- Urn mechanics have been reworked. + +Primary details: +- Delivering the urn now starts a king of the hill style capture point, rather than using melee to flip the urn back and forth. +- Both teams can progress at the same time, but the urn will only be fully claimed once there is only one team in the circle. +- The Urn runner now gets their normal 35% bonus bounty and stats immediately on initial deposit (rather than on deposit conclusion only if your team won it) + +Other Details: +- Urn overall bounty reduced by 20% (unreduced for trailing team) +- Progress radius is 20m +- Progress rate is fixed regardless the number of allies in the area +- Progress duration for favored/neutral/unfavored is 6/12/18s +- If Urn has been in progress for over 75s, it will give up and throw all the souls as orbs into the sky (they will float for a long time) +- Increased time allowed to run the urn before taking damage by 10s +- Trailing team sprint speed bonus from +4m to +5m +- Urn comeback resistance now also grants +35% Debuff Resistance ontop of the 35% Bullet and Spirit Resistance +- Urn comeback resistance aura reduced from 60m to match the 20m progress radius +- Drop off location is above the bridges on the side lanes, rather than below. \ No newline at end of file diff --git a/input-data/changelogs/raw/2026-06-11.txt b/input-data/changelogs/raw/2026-06-11.txt new file mode 100644 index 00000000..bd71b0cf --- /dev/null +++ b/input-data/changelogs/raw/2026-06-11.txt @@ -0,0 +1,105 @@ +- Urn give up time reduced from 75s to 60s +- Urn bounty reduced by 10% (unreduced for trailing team) + +- Breakables health permanent bonus reduced from 15/25/35 to 15/20/30 for level 1/2/3 + +- Kill comeback bounty values increased by 8% + +- Street Brawl: All ability and item range/radius values are reduced by 10% + +- Opening Rounds: Conditional Weapon Damage bonus reduced from 30% to 25% +- Opening Rounds: Spirit Power increased from +4 to +7 +- Arcane Surge: Fixed various cases with the bonuses not working + +- Apollo: Disengaging Sigil velocity increased by 50% +- Apollo: Disengaging Sigil velocity's vertical:horizontal ratio changed from 1.5:1 to 1:1 +- Apollo: Disengaging Sigil now allows input to alter the direction apollo launches himself (A/D biases to the left/right and W/S affect how much backwards motion is applied +- Apollo: Disengaging Sigil T1 changed from "+30 Damage" to "Gain +25% Fire Rate and Bullet Speed for 8s" +- Apollo: Disengaging Sigil T2 changed from "Gain +30% Fire Rate and +50% Bullet Speed for 10s" to "On Player Hit: +1 Stamina restored and resets Air Jump/Dash limit" +- Apollo: Disengaging Sigil T3 changed from "On Player Hit: +2 stamina restored and reset Air Jump/Dash limit" to "Recast within 4s" +- Apollo: Flawless Advance now allows Apollo to parry during it + +- Bebop: Exploding Uppercut T3 increased from +17% Missing Health to +18% +- Bebop: Fixed Sticky Bomb T3 duration ending once the bomb went off rather than the 5s duration +- Bebop: Sticky Bomb T3 changed from "On Cast: +5m Move Speed and +20% Fire Rate for 5s" to "On Cast: +5m Move Speed and +25% Debuff Resistance for 6s" (applies retroactively) + +- Calico: Gloom Bombs melee resist debuff now stacks additively +- Calico: Gloom Bombs T2 increased from -5% Melee Resist for 5s to -6% for 6s +- Calico: Gloom Bombs melee resist now applies on impact rather than explosion +- Calico: Return to Shadows damage increased from 140 to 150 +- Calico: Return to Shadows T2 damage increased from +65 to +75 +- Calico: Return to Shadows T3 heal increased from 350 to 450 + +- Doorman: Call Bell explosion damage spirit scaling reduced from 1.3 to 1.2 +- Doorman: Call Bell T3 spirit scaling reduced from 0.4 to 0.35 + +- Graves: Jar of Dead collection rate reduced by 20% (takes longer to gain a charge) +- Graves: Jar of Dead damage reduced from 17+0.27 to 16+0.25 +- Graves: Jar of Dead bounty increased from 5+0.25/boon to 7+0.5/boon +- Graves: Grasping Hands T3 Immobilize duration reduced from +1s to +0.75s + +- Grey Talon: Gun cycle time increased from 0.5775 to 0.6 (~4% DPS nerf) +- Grey Talon: Bullet damage growth reduced from +1.0 to +0.85 +- Grey Talon: Rain of Arrows cooldown increased from 22s to 23s +- Grey Talon: Rain of Arrows T2 reduced from -13s Cooldown to -12s + +- Holliday: Powder Keg now has an alt cast behavior to place the barrel at her feet +- Holliday: Powder Keg various improvements to the launch angles, velocities and feel of casting +- Holliday: Powder Keg now starts with 2 charges +- Holliday: Powder Keg Charge Time increased from 3.5s to 7s +- Holliday: Powder Keg spirit scaling reduced from 1.6 to 1.4 +- Holliday: Powder Keg T2 changed from "+58 Damage" to "+1 Charge" +- Holliday: Powder Keg T3 changed from "+2 Charges and +0.4s Displacement" to "+100 Damage, +0.5 Spirit Scaling and -5s Charge Time" +- Holliday: Fixed various issues with placing bounce pad on elevated areas +- Holliday: Bounce Pad landing radius reduced from 12m to 9m +- Holliday: Bounce Pad T3 changed from "+68 Stomp Damage and Improved Spirit Scaling" to "+0.7s Stomp Stun" (only triggers from Holliday) + +- Infernus: Fixed Afterburn Max duration refreshing not properly accounting for both Debuff Resist and +Ability Duration +- Infernus: Fixed Concussive Combustion cooldown not updating when getting the T2 or other CD reducing items when the ability is on cooldown + +- Ivy: Fixed a bug where Stone Form could sometimes do significantly more damage than intended + +- McGinnis: Mini Turret DPS rescaled from 30+0.39 to 24+0.42 (break even at 200 spirit power) +- McGinnis: Mini Turrets T3 Fire Rate reduced from +30% to +25% +- McGinnis: Fixed some rare cases where Heavy Barrage would stop working +- McGinnis: Heavy Barrage DPS reduced from 22.5 to 21 + +- Paige: Rallying Charge distance for max amp reduced from 350m to 250m +- Paige: Rallying Charge T2 increased from -30s Cooldown to -50s +- Paige: Plot Armor barrier spirit scaling increased from 1.3 to 1.5 +- Paige: Plot Armor T3 barrier spirit scaling increased from +0.3 to +0.5 + +- Pocket: Bullet damage growth reduced from +0.2 to +0.16 +- Pocket: Flying Cloak T3 reduced from -13s Cooldown to -12s +- Pocket: Affliction now does half damage on objectives + +- Seven: Bullet damage growth reduced from 0.337 to 0.24 +- Seven: Power Surge T3 reduced from +12s Duration to +10s +- Seven: Storm Cloud now hits breakables + +- Shiv: Killing Blow now has +30% more cooldown whenever it does not impact a player + +- Silver: Lycan Curse cooldown increased from 40s to 70s +- Silver: Lycan Curse T3 no longer heals + +- Viscous: Puddle Punch T2 increased from +40% Lifesteal to +60% +- Viscous: Goo Ball T2 now also increases Bullet and Spirit Resist by +10% + +- Victor: Aura of Suffering radius reduced from 9m to 8m +- Victor: Aura of Suffering T3 now also increases radius by +1m +- Victor: Fixed some client performance issues when using Aura of Suffering +- Victor: Shocking Reanimation cooldown increased from 230s to 240s +- Victor: Shocking Reanimation T3 reduced from -120s Cooldown to -110s +- Victor: Shocking Reanimation T3 increased from +150 Damage to +175 + +- Vindicta: Assassinate Max Bonus Damage spirit scaling increased from 1.7 to 2.0 + +- Warden: Bullet falloff reduced from 20m->58m to 18m->47m +- Warden: Alchemical Flask spirit scaling reduced from 0.73 to 0.63 +- Warden: Alchemical Flask T2 reduced from +40 Damage to +35 +- Warden: Alchemical Flask T2 increased from -20% Weapon Damage to -25% +- Warden: Willpower spirit scaling increased from +0.5 to +0.8 +- Warden: Binding Word T3 reduced from -18s Cooldown to -14s +- Warden: Last Stand lifesteal increased from 65% to 75% +- Warden: Last Stand T2 increased from -30s Cooldown to -35s +- Warden: Last Stand T3 increased from +3s Duration to +4s \ No newline at end of file diff --git a/input-data/changelogs/raw/2026-06-30.txt b/input-data/changelogs/raw/2026-06-30.txt new file mode 100644 index 00000000..c0eb4076 --- /dev/null +++ b/input-data/changelogs/raw/2026-06-30.txt @@ -0,0 +1,187 @@ +[ Urn / King of the Hill ] + +- King of the Hill objective has been rethemed and renamed to "Unstable Rift" +- Unstable Rift no longer requires an Urn delivery to trigger the start of the event. It now has a variable start time of +/- 1 minute. There is a global announcement 25s before it spawns. 60s before that there is also an in-world only visual effect to let you know which lane it'll randomly spawn in. +- Unstable Rift spawn interval increased from every 6 minutes to every 7 minutes +- Unstable Rift comeback bonuses (resists in the area, extra comeback bounty, and trailing/leading team claiming timer) now all scale linearly based on the amount of NW they are trailing (scales from no bonus at 0% NW behind to full bonus at 15% NW behind) +- The team that secures the Unstable Rift now also has a wave of 5 Rift Troopers sent down from the rift location. When trailing, spawns up to 12 troopers instead (using the same linear ramp from 0% NW to -15% NW) +- Rift Troopers drop no souls when killed +- Rift Troopers spawn with 100% extra health and damage (they also look larger and have custom VFX). Their bonus health and damage grow slightly per event (first one is 100%, then 120%, 140%, etc) +- Any players in the Unstable Rift Zone are now revealed on the minimap +- Unstable Rift bounty increased by 35% (to account for the 35% loss with no urn runner bounty, overall objective bounty is unchanged) +- Unstable Rift no longer grants +1 permanent buff to each player (there is also no +3 buffs to the urn runner) + +- Urn running is now its own objective. Spawns on either end of the map (where the previous neutral pickup locations were), and gets delivered to the opposite end. Spawns at 10/15/20/25/etc. +- Urn runner is no longer revealed and is not disarmed or silenced (going through a doorway or mirage teleport still drops it) +- Urn cannot be manually dropped. It gets dropped when you get hit with a light or heavy melee, are stunned or are killed. It stays where it drops and does not walk back home. +- Urn runner can no longer parry +- Urn bounty value starts decaying after 45s from being picked up. It drains gradually over another 45s and then the urn is removed when the bounty is empty. +- Urn is deposited immediately at the drop off point. +- Urn bounty when deposited is worth 250 + 70/min for just the urn runner. Still has the comeback bounty component. Upon deposit all of the souls are released as orbs into the air (if another ally secures the orbs, they share the bounty with the urn runner). +- Urn runner gets +4 permanent buffs on deposit +- Urn is now louder when it talks and is easier to hear by nearby enemies +- After 3 Minutes from spawn the Urn will start walking very slowly on his own towards the deposit location +- Urn runner's passive bonuses while holding it now scales based on how behind you are, rather than being on or off (same as the Unstable Rift 0%-15% NW linear scaling). Bonuses are the same as the previous urn runner bonuses. + +There are two debug commands you can use to test these out +- citadel_force_spawn_idol - This spawns the Urn +- citadel_koth_dev_test_spawn - This moves your hero and spawns the Unstable Rift in the early warning state then quickly transitions to the 25s broadcasted timer + +[ General ] + +- Reworked Boon reward table + +- Health per boon reduced by 5% +- All ultimate base and upgrade cooldowns nerfed by 15% (rounded to the nearest multiple of 5) +- Rejuv buff duration reduced from 4 minutes to 3 +- Respawn ramp changed from 8-30s from 5-19m to 8-35s from 5-20m +- Guardian bounty reduced from 1500 to 1250 +- Walker bounty reduced from 4000 to 3500 +- First blood bounty reduced from 150 to 125 +- Base kill bounty reduced from 250 to 200 (max is still 2200 at 40m) +- Trooper bounty rescaled from 116 + 1.16/m to 100 + 2/m (less before 20m, more after) +- Trooper bounty ratio in the deniable flying orb increased from 40% to 50% +- Minimum unsecured souls allowed to drop reduced from 150 to 50 + 5/min + +- Dash Jump input window increased from 0.2s to 0.25s duration (start time: 0.3s to end time: 0.55s) +- Dash Jump distance increased from 18m to 19m +- Dash Jump vertical impulse increased from 400 to 425 + +- Jump Pads on top of the double sinner buildings have been removed + +- Hovering an item in the shop now shows you a preview for your investment bars + +- Street Brawl: No longer locks you out of getting off the zipline in the first few seconds +- Street Brawl: Increased item randomness a bit +- Street Brawl: Fixed Enhanced Sharpshooter and Enhanced Spiritual Overflow not maintaining the bonuses from their enhanced components + +[ Items ] + +- Mystic Shot: Cooldown increased from 8s to 9s +- Toxic Bullets: Bleed damage increased from 1.7% to 1.9% +- Scourge: Max Health DPS reduced from 3.5% to 2.6% +- Scourge: Max Health DPS now scales with spirit power (0.0055) +- Cursed Relic: Damage Penalty increased from -10% to -14% + +[ Heroes ] + +- Abrams: Siphon Life T3 reduced from +3m Radius to +2m +- Abrams: Shoulder Charge T3 reduced from -20s Cooldown to -18s + +- Apollo: Riposte T1 increased from -7s Cooldown to -8s +- Apollo: Riposte grace window to target after channel increased from +1s to +1.3s +- Apollo: Riposte cast range increased from 25m to 35m +- Apollo: Riposte targeting angle increased from 70 to 90 +- Apollo: Flawless Advance Heal on hero hit spirit scaling increased from 1 to 1.3 +- Apollo: Itani Lo Sahn T3 increased from +40% Bonus Damage to +50% + +- Billy: Bullet damage per boon reduced by 10% +- Billy: Bashdown melee scaling reduced from 1.1 to 0.9 +- Billy: Bashdown T3 reduced from 60% Heavy Melee damage to 50% +- Billy: Rising Ram T3 Max HP Damage reduced from 10% to 8% +- Billy: Rising Ram T3 Max HP Damage spirit scaling increased from 0.017 to 0.035 (break even at 111 spirit power) + +- Celeste: Dazzling Trick cooldown reduced from 35s to 32s +- Celeste: Radiant Daggers buff duration increased from 25s to 30s +- Celeste: Radiant Daggers spirit scaling increased from 0.56 to 0.63 +- Celeste: Shining Wonder bounce range increased from 15.5m to 16.5m +- Celeste: Shining Wonder damage increased by 10% + +- Doorman: Doorway cooldown increased from 40s to 45s + +- Drifter: Health per boon increased from 41 to 43 (global hp boon reduction is after this) +- Drifter: Rend cast time reduced from 0.5s to 0.4s +- Drifter: Rend post cast time reduced from 0.5s to 0.4s +- Drifter: Rend T2 increased from -7s Cooldown to -8s +- Drifter: Bloodscent T1 increased from +2m/s while near an isolated enemy to +3m/s +- Drifter: Bloodscent T2 increased from 18% missing health heal to +24% + +- Dynamo: Rejuvenating Aurora regeneration increased from 25/s to 30/s + +- Grey Talon: Rain of Arrows cooldown increased from 23s to 25s +- Grey Talon: Guided Owl permanent spirit bonus reduced from 10 to 8 + +- Holliday: Powder Keg velocity reduced slightly +- Holliday: Powder Keg charge delay increased from 7s to 7.5s +- Holliday: Powder Keg spirit scaling reduced from 1.4 to 1.2 +- Holliday: Powder Keg T3 reduced from +100 Damage to +80 + +- Haze: Sleep Dagger T1 increased from -8% Bullet Resist to -10% +- Haze: Sleep Dagger T2 increased from -17s Cooldown to -18s +- Haze: Smoke Bomb T3 increased from +40% Bullet Lifesteal to +50% + +- Ivy: Entangling Thorns spirit scaling increased from 0.45 to 0.55 +- Ivy: Kudzu Connection Replicated Healing per boon scale increased from +0.5 to +0.85 +- Ivy: Air Drop cooldown reduction when used on allies increased from -25% to -30% + +- Lash: Ground Strike cooldown reduced from 21s to 18s +- Lash: Flog angle increased from 30 to 38 + +- Mina: Rake missing health as damage increased from 5% to 6% +- Mina: Sanguine Retreat T3 now also increases range by +3m +- Mina: Nox Nostra damage increased from 4.45 to 4.6 +- Mina: Nox Nostra T1 damage increased from +1.74 to +1.9 + +- Mo & Krill: Sand Blast T2 slow increased from -25% to -30% +- Mo & Krill: Sand Blast T3 increased from -20s Cooldown to -25s +- Mo & Krill: Combo bonus max health per kill increased from 30+1/boon to 40+2/boon + +- Paige: Health per boon increased from 29 to 33 +- Paige: Bookwyrm T1 changed from "+2s Trail Duration and +1m Radius" to "-12s Cooldown" +- Paige: Bookwyrm T2 changed from "+10m Range and -12s Cooldown" to "+1 Charge, +1m Radius and +2s Trail Duration" +- Paige: Bookworm T3 changed from "+100 Damage, +30 DPS and +1 Charge" to "+100 Damage, +30 DPS and +12m Travel Range" +- Paige: Plot Armor T1 fire rate spirit scaling increased from 0.13 to 0.16 +- Paige: Plot Armor T3 increased from 75% Barrier to 100% +- Paige: Rallying Charge is now properly counted as a "miss" (for the -50% CD Reduction) if the only thing that was impacted were non-heroes +- Paige: Rallying Charge T3 Max Amp increased from +50% to +70% + +- Paradox: Kinetic Carbine T2 now also increases move speed spirit scaling (0.06) +- Paradox: Kinetic Carbine T3 changed from affecting Max Damage Scaling to affecting both Min and Max Damage Scaling + +- Pocket: Bullet damage per boon reduced from 0.16 to 0.14 +- Pocket: Flying Cloak T3 reduced from -12s Cooldown to -11s + +- Rem: Tag Along healing per second spirit scaling reduced from 0.66 to 0.4 +- Rem: Tag Along T3 missing health spirit scaling reduced from +0.02 to +0.016 +- Rem: Lil Helpers Spirit Resist reduced from 15% to 12% +- Rem: Lil Helpers T1 changed from "+1 Helper" to "+1 Helper and +1.5m/s Move Speed" +- Rem: Lil Helpers T2 changed from "+8% Spirit Resist and +1.5m/s Move Speed" to "+1 Helper and +15% Trooper Damage and Resist" +- Rem: Lil Helpers T3 changed from "+2 Helpers and +20% Trooper Damage and Resist" to "+1 Helper and +15% Spirit Resist" + +- Seven: Lightning Ball charge delay reduced from 7s to 6s +- Seven: Storm Cloud time to expand increased from 1.5s to 3.5s +- Seven: Storm Cloud damage interval increased from 0.25s to 0.3s (DPS unchanged) +- Seven: Storm Cloud T2 now also increases Initial Radius by +5m + +- Shiv: Alt Fire damage now has a custom value per boon (+0.2) +- Shiv: Alt Fire ammo consumed per shot increased from 3 to 5 +- Shiv: Alt Fire knockback movement is now disabled by slowing hex state +- Shiv: Serrated Knives cooldown reduced from 18s to 16s +- Shiv: Slice and Dice now deals +25 light melee damage (75 total) instead of 60 spirit damage +- Shiv: Slice and Dice changed from -6% Spirit Resist to +4% Damage Amp +- Shiv: Bloodletting T2 changed from "+15% Incoming Damage Deferred" to "+35% Deferred Damage Cleared" +- Shiv: Bloodletting T3 changed from "+50% Deferred Damage Cleared" to "+15% Incoming Damage Deferred" +- Shiv: Killing Blow now deals damage to troopers and neutrals along the way +- Shiv: Killing Blow executing a hero now instantly fills the rage bar +- Shiv: Killing Blow now has the T3 "recast within 20s on a hero kill" as part of the base ability +- Shiv: Killing Blow Full Rage Damage Bonus reduced from +12% to +8% +- Shiv: Killing Blow range reduced from 18m to 12m +- Shiv: Killing Blow T1 now also increases range by +6m +- Shiv: Killing Blow T2 increased from +10% Full Rage Bonus Damage to +16% +- Shiv: Killing Blow T3 increased from +5% Enemy health Threshold to +10% + +- Vindicta: Stake T2 reduced from -20s Cooldown to -22s +- Vindicta: Crow Familiar T2 increased from -12s Cooldown to -16s +- Vindicta: Crow Familiar collision radius between each crow increased slightly +- Vindicta: Assassinate Max Bonus Damage spirit scaling increased from 2 to 2.3 + +- Viscous: Splatter T3 spirit scaling reduced from +1.1 to +1.0 +- Viscous: Puddle Punch T3 increased from -12s Cooldown to -14s +- Viscous: Goo Ball T3 increased from +6s Duration to +7s + +- Vyper: Screwjab Dagger T3 now also reduces charge delay from 4s to 2s +- Vyper: Slither T3 spirit scaling increased from 0.6 to 0.8 + +- Warden: Bullet damage per boon reduced from 0.34 to 0.28 +- Warden: Willpower T2 increased from -22s Cooldown to -24s +- Warden: Willpower T3 increased from +2.5 spirit power scaling to +2.7 \ No newline at end of file diff --git a/input-data/changelogs/raw/2026-07-01.txt b/input-data/changelogs/raw/2026-07-01.txt new file mode 100644 index 00000000..1523f88b --- /dev/null +++ b/input-data/changelogs/raw/2026-07-01.txt @@ -0,0 +1,5 @@ +- Shiv: Alt Fire ammo cost reduced from 5 to 4 +- Shiv: Weapon now has fixed pellet spread +- Shiv: Slice and Dice is now back to doing spirit damage and reducing Spirit Resistance from enemies +- Shiv: Slice and Dice damage increased from 60 to 75 +- Shiv: Killing Blow T3 reduced from +10% Enemy Health Threshold to +8% \ No newline at end of file diff --git a/input-data/changelogs/raw/2026-07-09.txt b/input-data/changelogs/raw/2026-07-09.txt new file mode 100644 index 00000000..47ad0b1a --- /dev/null +++ b/input-data/changelogs/raw/2026-07-09.txt @@ -0,0 +1,70 @@ +- Urn Runner sprint bonus reduced from +2m to 0 (trailing bonus reduced from +7m to +5m) +- Urn Runner move speed bonus reduced from +3.5m to +2m +- Urn Runner Stamina Recovery increased from +15% to +25% +- Urn talking frequency increased from every 8s to every 6s +- Urn talking sound distance increased (easier to hear a nearby Urn Runner) + +- Unstable Rift warning time reduced from 25s to 20s +- Rift Troopers now have Spirit Resist (30/35/40/45%) +- Rift Troopers now have Melee Resistance (25%) +- Rift Troopers spawn interval increased from every 0.3s to 0.5s (spawns slightly more staggered) +- Unstable Rift comeback resist aura radius increased from 20m to 35m +- Rift Troopers max comeback count increased from 12 to 14 + +- Objectives bounty split for nearby heroes reduced from 40% to 30% (this means slightly more portion of the bounty is split team wide rather than towards an individual player) + +- Scourge: Max Health Per Second reduced from 2.6% to 2.5% + +- Billy: Rising Ram T3 spirit scaling reduced from 0.035 to 0.03 + +- Doorman: Bullet damage rescaled from 29.26 + 1.1/boon to 26 + 1.19 +- Doorman: Crit scaling reduced from -25% to -30% +- Doorman: Hotel Guest T3 increased from 13s Cooldown on Failure to Check Out to 15s + +- Drifter: Bullet damage per boon reduced from +0.616 to +0.49 +- Drifter: Bloodscent T3 reduced from +12% to +11% +- Drifter: Stalker's Mark cooldown increased from 20s to 24s +- Drifter: Stalker's Mark T2 increased from -8s Cooldown to -12s +- Drifter: Stalker's Mark T3 reduced from +2% Bleed to +1.5% + +- Graves: Jar of Dead T1 heal spirit scaling increased from 0.1 to 0.16 +- Graves: Jar of Dead now build up faster from neutral creeps +- Graves: Grasping Hands immobilize duration reduced from 1.25s to 1s +- Graves: Grasping Hands T3 cooldown increased from -10s to -14s +- Graves: Essence Theft now procs from melee hits +- Graves: Essence Theft T3 now also increases Max Steal Targets from 3 to 4 +- Graves: Borrowed Decree T1 changed from "-15s Cooldown" to "-15s Cooldown and +25% Ghoul Speed" +- Graves: Borrowed Decree T2 changed from "+25% Ghoul Speed and +10s Duration" to "-0.3s Time to Spawn and +10s Duration" + +- Haze: Sleep Dagger T2 now also applies 15 Fixation Stacks +- Haze: Sleep Dagger T2 reduced from -18s Cooldown to -17s + +- Holliday: Powder Keg spirit scaling reduced from 1.2 to 1.05 + +- Mirage: Fire Scarabs T3 spirit scaling reduced from +0.17 to +0.13 +- Mirage: Dust Devil T3 spirit scaling reduced from +1 to +0.6 +- Mirage: Djinn's Mark cooldown reduced from 3s to 2.75 +- Mirage: Djinn's Mark T3 cooldown reduced from -1s Cooldown to -0.75s +- Mirage: Traveler T3 increased from -70s Cooldown to -90s + +- Pocket: Affliction duration reduced from 11s to 10s + +- Shiv: Serrated Knives T3 spirit scaling reduced from +0.09 to +0.07 +- Shiv: Slice and Dice spirit scaling reduced from 1.44 to 1.2 +- Shiv: Slice and Dice T2 reduced from -6% Spirit Resist to -4% +- Shiv: Bloodletting cooldown increased from 20s to 25s +- Shiv: Bloodletting T1 increased from -10s Cooldown to -15s +- Shiv: Bloodletting Damage Cleared reduced from 35% to 30% +- Shiv: Bloodletting T2 Damage Cleared increased from 35% to 40% +- Shiv: Killing Blow Health Threshold reduced from 20% to 18% +- Shiv: Killing Blow T3 Health Threshold increased from +8% to +10% + +- Silver: Lycan Curse cooldown reduced from 80s to 60s + +- Victor: Shocking Reanimation cooldown reduced from 275s to 240s + +- Viscous: Splatter T3 spirit scaling reduced from +1.0 to +0.9 + +- Yamato: Flying Slash range reduced from 28m to 26m +- Yamato: Flying Slash T3 Charge Delay increased from 3s to 4s +- Yamato: Flying Slash T3 Ally Targeting now requires alt cast input \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index 52ba9822..0ae762f5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,53 +1,15 @@ # This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. -[[package]] -name = "beautifulsoup4" -version = "4.14.3" -description = "Screen-scraping library" -optional = false -python-versions = ">=3.7.0" -groups = ["main"] -files = [ - {file = "beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb"}, - {file = "beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86"}, -] - -[package.dependencies] -soupsieve = ">=1.6.1" -typing-extensions = ">=4.0.0" - -[package.extras] -cchardet = ["cchardet"] -chardet = ["chardet"] -charset-normalizer = ["charset-normalizer"] -html5lib = ["html5lib"] -lxml = ["lxml"] - -[[package]] -name = "bs4" -version = "0.0.2" -description = "Dummy package for Beautiful Soup (beautifulsoup4)" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "bs4-0.0.2-py2.py3-none-any.whl", hash = "sha256:abf8742c0805ef7f662dce4b51cca104cffe52b835238afc169142ab9b3fbccc"}, - {file = "bs4-0.0.2.tar.gz", hash = "sha256:a48685c58f50fe127722417bae83fe6badf500d54b55f7e39ffe43b798653925"}, -] - -[package.dependencies] -beautifulsoup4 = "*" - [[package]] name = "certifi" -version = "2026.5.20" +version = "2026.6.17" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897"}, - {file = "certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d"}, + {file = "certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db"}, + {file = "certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432"}, ] [[package]] @@ -216,41 +178,26 @@ files = [ [[package]] name = "distlib" -version = "0.4.1" +version = "0.4.3" description = "Distribution utilities" optional = false python-versions = "*" groups = ["dev"] files = [ - {file = "distlib-0.4.1-py2.py3-none-any.whl", hash = "sha256:9c2c552c68cbadc619f2d0ed3a69e27c351a3f4c9baa9ffb7df9e9cdc3d19a97"}, - {file = "distlib-0.4.1.tar.gz", hash = "sha256:c3804d0d2d4b5fcd44036eb860cb6660485fcdf5c2aba53dc324d805837ea65b"}, + {file = "distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b"}, + {file = "distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed"}, ] -[[package]] -name = "feedparser" -version = "6.0.12" -description = "Universal feed parser, handles RSS 0.9x, RSS 1.0, RSS 2.0, CDF, Atom 0.3, and Atom 1.0 feeds" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "feedparser-6.0.12-py3-none-any.whl", hash = "sha256:6bbff10f5a52662c00a2e3f86a38928c37c48f77b3c511aedcd51de933549324"}, - {file = "feedparser-6.0.12.tar.gz", hash = "sha256:64f76ce90ae3e8ef5d1ede0f8d3b50ce26bcce71dd8ae5e82b1cd2d4a5f94228"}, -] - -[package.dependencies] -sgmllib3k = "*" - [[package]] name = "filelock" -version = "3.29.1" +version = "3.29.5" description = "A platform independent file lock." optional = false python-versions = ">=3.10" groups = ["dev"] files = [ - {file = "filelock-3.29.1-py3-none-any.whl", hash = "sha256:85199dfd706869641b72b2e8955d5416a4b2b7dc4b0e8e6d97b4cc1299a6983b"}, - {file = "filelock-3.29.1.tar.gz", hash = "sha256:d97e6b1b9757569626c58caa07dc4beb1613f4a2938b1e8cc81afca398906c9e"}, + {file = "filelock-3.29.5-py3-none-any.whl", hash = "sha256:8af830889ba3a0ffcefbd6c7d2af8a54012058103771f2e10848222f476a1693"}, + {file = "filelock-3.29.5.tar.gz", hash = "sha256:6e6034c57a00a020e767f2614a5539863f056de7e7991d6d1473aef7ff73f156"}, ] [[package]] @@ -422,13 +369,13 @@ files = [ [[package]] name = "nuitka" -version = "4.1.2" +version = "4.1.3" description = "Python compiler with full language support and CPython compatibility" optional = false python-versions = "*" groups = ["build"] files = [ - {file = "nuitka-4.1.2.tar.gz", hash = "sha256:efc2359b171d7b63046ca8ec8dee57015c3466a9df74b68a049c2c1a7e93ecee"}, + {file = "nuitka-4.1.3.tar.gz", hash = "sha256:838ff8899dc2f0b652d4fcf6c5d7466cb7ad5abcb005668ac622d1e40f4d8a8d"}, ] [package.extras] @@ -475,103 +422,99 @@ testing = ["pytest"] [[package]] name = "pillow" -version = "12.2.0" +version = "12.3.0" description = "Python Imaging Library (fork)" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f"}, - {file = "pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97"}, - {file = "pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff"}, - {file = "pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec"}, - {file = "pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136"}, - {file = "pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c"}, - {file = "pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3"}, - {file = "pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa"}, - {file = "pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032"}, - {file = "pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5"}, - {file = "pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024"}, - {file = "pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab"}, - {file = "pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65"}, - {file = "pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7"}, - {file = "pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e"}, - {file = "pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705"}, - {file = "pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176"}, - {file = "pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b"}, - {file = "pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909"}, - {file = "pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808"}, - {file = "pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60"}, - {file = "pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe"}, - {file = "pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5"}, - {file = "pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421"}, - {file = "pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987"}, - {file = "pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76"}, - {file = "pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005"}, - {file = "pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780"}, - {file = "pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5"}, - {file = "pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5"}, - {file = "pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940"}, - {file = "pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5"}, - {file = "pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414"}, - {file = "pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c"}, - {file = "pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2"}, - {file = "pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c"}, - {file = "pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795"}, - {file = "pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f"}, - {file = "pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed"}, - {file = "pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9"}, - {file = "pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed"}, - {file = "pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3"}, - {file = "pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9"}, - {file = "pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795"}, - {file = "pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e"}, - {file = "pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b"}, - {file = "pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06"}, - {file = "pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b"}, - {file = "pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f"}, - {file = "pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612"}, - {file = "pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c"}, - {file = "pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea"}, - {file = "pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4"}, - {file = "pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4"}, - {file = "pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea"}, - {file = "pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24"}, - {file = "pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98"}, - {file = "pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453"}, - {file = "pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8"}, - {file = "pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b"}, - {file = "pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295"}, - {file = "pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed"}, - {file = "pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae"}, - {file = "pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601"}, - {file = "pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be"}, - {file = "pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f"}, - {file = "pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286"}, - {file = "pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50"}, - {file = "pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104"}, - {file = "pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7"}, - {file = "pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150"}, - {file = "pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1"}, - {file = "pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463"}, - {file = "pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3"}, - {file = "pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166"}, - {file = "pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe"}, - {file = "pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd"}, - {file = "pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e"}, - {file = "pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06"}, - {file = "pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43"}, - {file = "pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354"}, - {file = "pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1"}, - {file = "pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb"}, - {file = "pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f"}, - {file = "pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d"}, - {file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f"}, - {file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e"}, - {file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0"}, - {file = "pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1"}, - {file = "pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e"}, - {file = "pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5"}, + {file = "pillow-12.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6c0016e7b354317c4e9e525b937ac8596c38d2d232b419529b9cd7a1cd46e39a"}, + {file = "pillow-12.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bcc33feacfaefce60c12fd500a277533bdc02b10a19f7f6d348763d8140bbba7"}, + {file = "pillow-12.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5594fc43d548a7ed94949d139aa1341b270f1863f11cfd37f5a6c8b778a6b67f"}, + {file = "pillow-12.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f0606c8bf2cdefea14a43530f7657cbbb7ecf1c4222512492ef4a4434a9501ec"}, + {file = "pillow-12.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:85f998ea1848bc6757289e739cfbdda3a04adfd58b02fc018ce54d754a5ce468"}, + {file = "pillow-12.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:25b9b82bb22e6e2b3cd07b39c68b7b862001226cb3dff7130d1cb914121b39ed"}, + {file = "pillow-12.3.0-cp310-cp310-win32.whl", hash = "sha256:37dc8f7bbb66efe481bb60defacef820c950c24713fb44962ed6aa2a50966de1"}, + {file = "pillow-12.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:300557495eb45ebb8aec96c2da9c4be642fbf7cd937278b4013ba894ea8eb0eb"}, + {file = "pillow-12.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:514435a37670e3e5e08f3945b68718b6ed329bb84367777e16f9f4dfe1e61a0f"}, + {file = "pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756"}, + {file = "pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6"}, + {file = "pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd"}, + {file = "pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd"}, + {file = "pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c"}, + {file = "pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5"}, + {file = "pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b"}, + {file = "pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a"}, + {file = "pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26"}, + {file = "pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965"}, + {file = "pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7"}, + {file = "pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9"}, + {file = "pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91"}, + {file = "pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c"}, + {file = "pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df"}, + {file = "pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f"}, + {file = "pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09"}, + {file = "pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510"}, + {file = "pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89"}, + {file = "pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace"}, + {file = "pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec"}, + {file = "pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66"}, + {file = "pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35"}, + {file = "pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65"}, + {file = "pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3"}, + {file = "pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a"}, + {file = "pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e"}, + {file = "pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f"}, + {file = "pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8"}, + {file = "pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b"}, + {file = "pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330"}, + {file = "pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217"}, + {file = "pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930"}, + {file = "pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8"}, + {file = "pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0"}, + {file = "pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321"}, + {file = "pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b"}, + {file = "pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198"}, + {file = "pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130"}, + {file = "pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a"}, + {file = "pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d"}, + {file = "pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838"}, + {file = "pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e"}, + {file = "pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17"}, + {file = "pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385"}, + {file = "pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c"}, + {file = "pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d"}, + {file = "pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931"}, + {file = "pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7"}, + {file = "pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c"}, + {file = "pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45"}, + {file = "pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139"}, + {file = "pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402"}, + {file = "pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c"}, + {file = "pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f"}, + {file = "pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701"}, + {file = "pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace"}, + {file = "pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4"}, + {file = "pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39"}, + {file = "pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71"}, + {file = "pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827"}, + {file = "pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5"}, + {file = "pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658"}, + {file = "pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf"}, + {file = "pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64"}, + {file = "pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e"}, + {file = "pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777"}, + {file = "pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1"}, + {file = "pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9"}, + {file = "pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8"}, + {file = "pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418"}, + {file = "pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3"}, + {file = "pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a"}, + {file = "pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce"}, ] [package.extras] @@ -579,7 +522,7 @@ docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybut fpx = ["olefile"] mic = ["olefile"] test-arrow = ["arro3-compute", "arro3-core", "nanoarrow", "pyarrow"] -tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma (>=5)", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] +tests = ["coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "setuptools", "trove-classifiers (>=2024.10.12)"] xmp = ["defusedxml"] [[package]] @@ -615,14 +558,14 @@ virtualenv = ">=20.10.0" [[package]] name = "python-discovery" -version = "1.4.0" +version = "1.4.3" description = "Python interpreter discovery" optional = false python-versions = ">=3.8" groups = ["dev"] files = [ - {file = "python_discovery-1.4.0-py3-none-any.whl", hash = "sha256:26ed78d703e234879a66244c7d4114563fb13ec5cd30a2d1357e5fb4850782da"}, - {file = "python_discovery-1.4.0.tar.gz", hash = "sha256:eb8bc7daad3c226c147e45bb4e970a1feb1bf4048ee178e6db59e197b8010ce3"}, + {file = "python_discovery-1.4.3-py3-none-any.whl", hash = "sha256:b6e1e4a7d9e3f6948c39746ffe8218225162d738ba39d05ab1d2f6c1cac4878c"}, + {file = "python_discovery-1.4.3.tar.gz", hash = "sha256:ad57d7045a862460d4a235986c33f13ed707d3aeb9153fa47eb7dfd0d4673289"}, ] [package.dependencies] @@ -748,126 +691,126 @@ files = [ [[package]] name = "regex" -version = "2026.5.9" +version = "2026.6.28" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "regex-2026.5.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a9e1328e17c84c1a5d22ec9f785ecef4a967fab9a42b6a8dc3bcbebd0a0c9e44"}, - {file = "regex-2026.5.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bfe1ce50cbfb569d74e1e4337da6468961f31dbea55fd85aa5de59c0947a805a"}, - {file = "regex-2026.5.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:15ee42209947f4ca045412eae98416317238163618ace2a8e54f99586a466733"}, - {file = "regex-2026.5.9-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4bb445ff3f725f59df8f6014edb547ee928ec7023a774f6a39a3f953038cbb2"}, - {file = "regex-2026.5.9-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:446ddd671e43ab535810c4b21cff7104945c701d4a14d1e6d1cd6f4e445a8bea"}, - {file = "regex-2026.5.9-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7b92817338591505f282cf3864c145244b1edcf5381d237038df955001091538"}, - {file = "regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b8a143aca6c39b446ea8092cde25cc8fe9304d4f5fecfbc1a9dbb0282703c2"}, - {file = "regex-2026.5.9-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0f03aa6898aaaac4592479821df16e68e8d0e29e903e65d8f2dfb2f19028a989"}, - {file = "regex-2026.5.9-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ed457d8e98ae812ed7732bef7bf78de78e834eae0372a74e23ca90ef21d910f9"}, - {file = "regex-2026.5.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71b61c5bfe1c806332defc42ad6c780b3c55f661986d7f40283a3a88274b4c00"}, - {file = "regex-2026.5.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:3b1e39888c5e0c7d92cea4fc777396c4a90363b05de75d02eb459a4752200808"}, - {file = "regex-2026.5.9-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:6ba42b2e7e7f46cf68cc6a5ca36fa07959f9bbd9c6bdcc47b6ee76549a590248"}, - {file = "regex-2026.5.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:c010eb8caca74bdb40c07498d7ece26b4428fd3f04aa8a72c9ac6f79e8faaac6"}, - {file = "regex-2026.5.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a6a563446a41adc451393dc6b8e6ad87979efaee3c8738690a8d1b08ebead1b4"}, - {file = "regex-2026.5.9-cp310-cp310-win32.whl", hash = "sha256:954cc214c04663ee6d266fc61739cad83054683048de65c5bd1d640ad28098ac"}, - {file = "regex-2026.5.9-cp310-cp310-win_amd64.whl", hash = "sha256:b310768746dd314ea6e2ff4cc89ef215426813396ff4e94ee8e6f7096c8b6e03"}, - {file = "regex-2026.5.9-cp310-cp310-win_arm64.whl", hash = "sha256:19c16ceb4a267a8789e25733e583983eeab9f0f8664e66b0bd1c5d21f14c2d4b"}, - {file = "regex-2026.5.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ccf5249114cc3e772ecdd88a98a86eca0fd74c61ce32a94743758c083fc05d48"}, - {file = "regex-2026.5.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46f1326ca6e65b0879d23ca302c0f2415aad42ff0309b9c818e7949fe19a41d8"}, - {file = "regex-2026.5.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef31cbfe458e21c6122ba8150ff060e0c7789ed0d26eb423f25472584920b555"}, - {file = "regex-2026.5.9-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:992604d02e6d9c6d786c24a706a71ecffe1020fc1ef264044474cd81fa2c3919"}, - {file = "regex-2026.5.9-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c9411dd64ca95477225734a93dfc8583b51916b8d5942f99d6cac21e09965451"}, - {file = "regex-2026.5.9-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3dd4a3ff360dfb836fecdb93a4598f9d6e2ac81e3e397125145c6221bf58cf4c"}, - {file = "regex-2026.5.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a661a7d270a61f7cf460caee8b9fa2d5ef9e5c681234bcb9e0fe14f488e7dfc"}, - {file = "regex-2026.5.9-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f079e50a0d3cc3cd5091fa9ff45869a2e6b2cd35895731edafb0327901a8d86d"}, - {file = "regex-2026.5.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4ebe8f0b5ec5a5024dc4a4c59f444c4e9afc5f2abdbb8962065b75d27fb971f9"}, - {file = "regex-2026.5.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:97cf3bc1b7d7d2306772ec07366c80d9df00ff79e79cea32898883a646d2fae2"}, - {file = "regex-2026.5.9-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0f9eede6a5cbdc02d4978090186390936e1776a7d1359b21e41014c609880bcf"}, - {file = "regex-2026.5.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:01f0f5f55f4b64dacec85dc116d3c05fd23ad3ff037bbc73a2085775953c2611"}, - {file = "regex-2026.5.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1268eddd8486dc561d08eee1156e40aa3a8fe10f4bdec8fa653b455fcbffd12c"}, - {file = "regex-2026.5.9-cp311-cp311-win32.whl", hash = "sha256:8676474c07469d6f33dd1085ca2cd45f65785f32518f2b20e36d9953ca07f994"}, - {file = "regex-2026.5.9-cp311-cp311-win_amd64.whl", hash = "sha256:246de9d60aa3f8538b519834dd95cbf276ea263d6a7bd5a3666dc3fa0230505b"}, - {file = "regex-2026.5.9-cp311-cp311-win_arm64.whl", hash = "sha256:d726ca3f0d76969bf1e8e477d160d3d666bbf999f6860bd314889e5345782046"}, - {file = "regex-2026.5.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57eeeb05db7979413dec5438f2db21d7ecbba787cde7a711df1a6f6df672aa06"}, - {file = "regex-2026.5.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:398c521292f4c7fb807001dcd54694d3a1fcafc179a36ad9cc56f98df85930b6"}, - {file = "regex-2026.5.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f7a7c26137296beba7784de6eba69c6a93a63ccebc385e4962fe67e267a91225"}, - {file = "regex-2026.5.9-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6441cc660d76107934a09c22167200839a0e89604a6297f78a974e66e931d2c0"}, - {file = "regex-2026.5.9-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:91328f1c23d47595ca3ef0a7557fa129c5a23404b775c770697d2f35b33e0107"}, - {file = "regex-2026.5.9-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:93a7860539414dddaefba2b40f8771765ae17949d4c7182b876ce429e11a8309"}, - {file = "regex-2026.5.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd2810d22146b6d838acc5ec15602cb6b47920aa4e33015df3868eedfd20bab8"}, - {file = "regex-2026.5.9-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:daff2bdbaf1d23e52fdff7c0b7bc2048b68f978df6a4d107ac981f94caef2e66"}, - {file = "regex-2026.5.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4eeb011098fcb77af513dcef521a3dbecbf8849b1e38940759d293b7a93f5026"}, - {file = "regex-2026.5.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ea9c8ecfa1b73c73b626534d6626e5340d429630943672b8480724f44e84b962"}, - {file = "regex-2026.5.9-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:cd2846168eb9ee3c513902bc8225409cb1caab31d04728b145171fa1625d9621"}, - {file = "regex-2026.5.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39617fb0cde9c0e6306dc70e3bfc096f3da793219879f7ae7aa341a69fbdcf6d"}, - {file = "regex-2026.5.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fd03c4f0e33280d15cae17159b899245d6b7c53d21def19b263b39655061f5ce"}, - {file = "regex-2026.5.9-cp312-cp312-win32.whl", hash = "sha256:164eba9b755ea6f244b0d881196fbc1fac09714e9782c9e2732b813142033c8e"}, - {file = "regex-2026.5.9-cp312-cp312-win_amd64.whl", hash = "sha256:86f40a5d6444db30a125c9c9177e6b25dad981cbc37451fd838f145e6edac92e"}, - {file = "regex-2026.5.9-cp312-cp312-win_arm64.whl", hash = "sha256:96f5f58b54a063d7ea9dca08e1cf57bfe10499c4d579ee672da284f57f5f0070"}, - {file = "regex-2026.5.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d626b84406444b165fc0ba981604edea39f0588ff1f92baa23fe50799ea9afdb"}, - {file = "regex-2026.5.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d7bdc0ab8f3dd7e1b4f9ab88634e13374669db86bb3c72e8292f07ae313f539f"}, - {file = "regex-2026.5.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a8820737949116ffff55fe18f9fc644530063ba6ebfcb8314239416e78f1347c"}, - {file = "regex-2026.5.9-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0fbdbac82cb3e4450d0ccde7d7a35607f4cb2dd9fba4b8b69bfaf8c9fa6aed"}, - {file = "regex-2026.5.9-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57e8915c7986aa33d25e4d3629cef711cd2863f2961b10409f0c04cb8b7d9020"}, - {file = "regex-2026.5.9-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:508f56a89ba9cb26e4168cbc37dbd60a28d82430a9e18ad1d25fe0883c314ca2"}, - {file = "regex-2026.5.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6d189041f15691cfa2b6c4290448ec221244d225b3f5fe9e7771b34ffcdf6e2"}, - {file = "regex-2026.5.9-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e82db382b44d0111b22601c509c89f64434816c9e0eef9d1989cda8cc6ff1c04"}, - {file = "regex-2026.5.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2acfb48634f64996b57f90f39afa692ff362162722581921fe92239a59960f3c"}, - {file = "regex-2026.5.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d29eebfc9525db68cad3c97eedd7f754fa265aa5cd0cf4f863b2421e1b48fc9f"}, - {file = "regex-2026.5.9-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:debb893095e944091c16e641a6e33c1b0f4cb61ab945ec5afbf53ce7068834d8"}, - {file = "regex-2026.5.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:d659eee77986549c9ea45b861c7567e44d6287c3dc9a4565478853f7b9fe2ff6"}, - {file = "regex-2026.5.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2efa205e6d98b24d1f3ab395c11aa15cdf10935bca283d0285e0499c284fba21"}, - {file = "regex-2026.5.9-cp313-cp313-win32.whl", hash = "sha256:f3844f134e834076677dd369976e9f5068679fcb8e50102fdf6b7ac96a3ec127"}, - {file = "regex-2026.5.9-cp313-cp313-win_amd64.whl", hash = "sha256:3527bb4942d2c14552155406cdedd906567456821848aed1cb4933a391bf5eca"}, - {file = "regex-2026.5.9-cp313-cp313-win_arm64.whl", hash = "sha256:56a33f191f17d8c417f99945ebdc1e691d3af9605d86ec68c7e54a57e3e17af6"}, - {file = "regex-2026.5.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:01f28d868834624c934b8d2e0aa1c8341337e37831f4a012f18a5afcba4cbaf3"}, - {file = "regex-2026.5.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:48036f6374aaa79eb3b754ec29c61d1c6b1606749d705a13f8854fa2539671f6"}, - {file = "regex-2026.5.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b96350aa424e79d4fd6b567b344dcbe2b2d6bfc48dfe7717587e1fa6d43da6ff"}, - {file = "regex-2026.5.9-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f3af7a4903c5c04a11a196a5aa75cdd7dd3f8508132f9fb3259d9f5908e3b88"}, - {file = "regex-2026.5.9-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e87577720152d2caae19fe2baaf1f8d5ca12091e9e229f03915c37d1e4b9178"}, - {file = "regex-2026.5.9-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c8b9b9d294cfea3cd19c718ade7cc93492b2c4991abd9a68d0b3477ae6d8e100"}, - {file = "regex-2026.5.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:728d8bfd28a8845c8b6bc5dc7ce010453d206396786c0765c2740cb65f37791e"}, - {file = "regex-2026.5.9-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7e30b874d341fac767d7df5a0870540541c2c054b80cfaac116e8d367a8a7ff2"}, - {file = "regex-2026.5.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fd190e88a895a8901325fad284a3f74ea52b1da8525b76cc811fa9b1edf0ce2b"}, - {file = "regex-2026.5.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8e76e8161ad00694cfce6767d5dea860c6391ac5b83e5c3a39661e696f11fc7e"}, - {file = "regex-2026.5.9-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ddda5340e6c01a293027dd46232fa79eaff1b48058ce7a98f572b6445b088041"}, - {file = "regex-2026.5.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:205109e96b3cf5adf8f4cd62bedde9487feb282b9497a3535451e5a24cd706a0"}, - {file = "regex-2026.5.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dfbe4579b9f08036aa7d101d1835437a20783574ac66327e6b29b4018a138081"}, - {file = "regex-2026.5.9-cp313-cp313t-win32.whl", hash = "sha256:ed2c9e8068b614c574d8d30e543d617cf5379b0535d46f97ef00e904745a08b5"}, - {file = "regex-2026.5.9-cp313-cp313t-win_amd64.whl", hash = "sha256:b46b0f094dc1d3b90356c85a0bd2c9bafc4a6a190b9d6f8ddd5a033b6e088ed4"}, - {file = "regex-2026.5.9-cp313-cp313t-win_arm64.whl", hash = "sha256:872acc074bd29ffc9913ecdfedf6ea77502312ca44a4aa0d3779089c6069d8de"}, - {file = "regex-2026.5.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:1bd7587a2948b4085195d5a3374eaf4a425dc3e55784c038175355ecf3bbbf8a"}, - {file = "regex-2026.5.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:dea2e88e1cce4522496cce630e11e67b98b7076620bc4336c3f674bc21a375f4"}, - {file = "regex-2026.5.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2099f7e7ff7b6aa3192312650a56e91cc091e49d50b04e4f6f8b6e28b3b27f1c"}, - {file = "regex-2026.5.9-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecd353045824e4477562a2ac718c25799cdaaa41f7aa925a806a8a3e6848a5b9"}, - {file = "regex-2026.5.9-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:65c8c8c37377794bd5b2f3ebe51919042bf17aec802e23c833d89782ed0c78af"}, - {file = "regex-2026.5.9-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b73ab8afcf66c622db143d1c6fda4e58e4d537ee4f125229ad47b1ab80f34c0"}, - {file = "regex-2026.5.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0de5cf193997384ed2ca6f1cd4f78055b255d93d82d5a8cd6ba0d11c10b167e4"}, - {file = "regex-2026.5.9-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d641a8c9a61618047796d572a39a79b26167b0411d2c3031937b2fe2d081e2cf"}, - {file = "regex-2026.5.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:24b2355ef5cc9aa5b8f07d17704face1c166fdcc2290fa7bd6e6c925655a8346"}, - {file = "regex-2026.5.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a24852d3c29ad9e47593593d8a247c44ccc3d0548ef12c822d6ed0810affe676"}, - {file = "regex-2026.5.9-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:916714069da19329ef7de197dcbc77bb3104145c7c2c864dbfbe318f46b88b14"}, - {file = "regex-2026.5.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:fa411799ca8da32a8d38d020a88faa5b6f91657d284761352940ecf9f7c3bbdd"}, - {file = "regex-2026.5.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e6da47d679b7010ef27556b6e0f99771b744936db1792a10ceac6547ae1503e"}, - {file = "regex-2026.5.9-cp314-cp314-win32.whl", hash = "sha256:98bd73080e8756255137e1bd3f3f00295bbc5aa383c0e0f973920e9134d7c4ad"}, - {file = "regex-2026.5.9-cp314-cp314-win_amd64.whl", hash = "sha256:ff8d372ac2acdc048d1c19916f27ee61bc5722728458ba6ca5052f2c72d51763"}, - {file = "regex-2026.5.9-cp314-cp314-win_arm64.whl", hash = "sha256:e1d93bf647916292e8edcec150c07ddf3dc50179ccaf770c04a7f9e452155372"}, - {file = "regex-2026.5.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:83d0ee4a57d1c87cb549e195ec300b8f0ec3a82eba66d835e4e2ed8634fe4499"}, - {file = "regex-2026.5.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d3d7eb5c9a7f6df82ed3cfac9beb93882a5cbcb5b8b157b56cb2b3b276574ac1"}, - {file = "regex-2026.5.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:075160bf16658e16d35233300b8453aac25de4cbea808d22348b6979668e924d"}, - {file = "regex-2026.5.9-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45375819235558a4ff1c4971dc32881f022613abdb180128f5cb4768c1765a1c"}, - {file = "regex-2026.5.9-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ead4b163ac30a29574510cd4b3e2e985ac5290c05fc7095557d6a5f403fc31b5"}, - {file = "regex-2026.5.9-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8c6e4218fbdfbcd4f6c19efca40930d24a621bf4b48cb76bc6640543bd28ef20"}, - {file = "regex-2026.5.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6351571c8a42b505eb555c0dc47d740d0fb66977dc142919eea6f4325b7c56a0"}, - {file = "regex-2026.5.9-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:002205cafd2a9e78c6290c7d1df277bf3277b3b7a30e0b4bb0dac2e2e3f7cb2d"}, - {file = "regex-2026.5.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8abd33fef90b2a9efac5557d6033ca82d1195ed3a15fea5af15ba7b463c6a63b"}, - {file = "regex-2026.5.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:31037c82eccb44b7ea2e9e221d7c01429430e989a1f4b91ea5a855f6017b509a"}, - {file = "regex-2026.5.9-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5604dfd046dc37eca90250fc3be938b076c8059fa772ac0ed6f499b0f0fb0415"}, - {file = "regex-2026.5.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e1b1b4e496afbb24f4a62aba855ee4f88f25578927697b340702e48c9ee6bc2"}, - {file = "regex-2026.5.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:be3372b9df6ddecff6486d37e19095a7b4973137caf5512407a89f4455361f41"}, - {file = "regex-2026.5.9-cp314-cp314t-win32.whl", hash = "sha256:3ddd90103f9e5c471c49c7852ecc1fe27c7e45eb99e977aefe7caa4e779f4f58"}, - {file = "regex-2026.5.9-cp314-cp314t-win_amd64.whl", hash = "sha256:ca518ed29c46eecba6010b15f1b9a479314d2de409536e71b6a13aa04e3b8a77"}, - {file = "regex-2026.5.9-cp314-cp314t-win_arm64.whl", hash = "sha256:5e41809d2683fcde7d5a8c87a6567ba1fb1ce0de9f31bff578de00a4b2d76daa"}, - {file = "regex-2026.5.9.tar.gz", hash = "sha256:a8234aa23ec39894bfe4a3f1b85616a7032481964a13ac6fc9f10de4f6fca270"}, + {file = "regex-2026.6.28-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b83932645630965fd860fdb70ebbf964bf3e8007f08851ea424d01f8d35454a8"}, + {file = "regex-2026.6.28-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e81f1952355042e517dc9861ce65c676e4a098f42402993c40461786d1f794d4"}, + {file = "regex-2026.6.28-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2097591101d70bcc108af64c46f6066bb698ee067fec5f75beac0be317639311"}, + {file = "regex-2026.6.28-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31d7538a614b5842bf53ce329d07b43f97754ca7e6db8d69f347e071bce1c953"}, + {file = "regex-2026.6.28-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f5561e47bbe2b75373b695326507743fcdd4d2cc7f5022312024ccf39fa094e0"}, + {file = "regex-2026.6.28-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c10f2c5a55ab3dd8318d8ad5f11b530e2691c0edebebde7713066f484902c3fb"}, + {file = "regex-2026.6.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23f7e0cc60c72486b42a685f1ff4eec90d50d4fb05e4f9c7d5363b03aa02600d"}, + {file = "regex-2026.6.28-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:76493755f79a88d5ed2c9e63a41d3c05997e0a7ffbe76ed8c4ded8be35b8b14c"}, + {file = "regex-2026.6.28-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ff0f41a00f23ea5054acb61901380c41813d813eee3f80f800995710bcc52ecd"}, + {file = "regex-2026.6.28-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3c60b297292e7e1ef5d02a4759f9e452ee4c8bb95e168d8fd0b5db01bd806f9f"}, + {file = "regex-2026.6.28-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a7cf03c87f7b9cbc25a8894cf9be83818406677b6b391b003ec7c884923387b5"}, + {file = "regex-2026.6.28-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:418208ea0af51cfed4f46eb9b1ea7cfc990ca284f0084ecbd951460fb089421e"}, + {file = "regex-2026.6.28-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:7635fa2cddb917a6bbfac7890602573d2d8c4e470703b0640e6f86a988817ec3"}, + {file = "regex-2026.6.28-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7bb96c13d6cf5880d31bbef84ca701a64d738aa491c2b79975cc33f8ad00a31e"}, + {file = "regex-2026.6.28-cp310-cp310-win32.whl", hash = "sha256:56f05194c4843957dd8b3af87eb0c52d8cf0509e7f18e172d727f5f8ff840646"}, + {file = "regex-2026.6.28-cp310-cp310-win_amd64.whl", hash = "sha256:70710927033af3b54369f17aaba1343b97a23d0b1aa994fa1512b08b1b8c136a"}, + {file = "regex-2026.6.28-cp310-cp310-win_arm64.whl", hash = "sha256:ed7b30185ee3f8b9b053b0be567b4d226016e2afbebc17fde1c6a4580937b688"}, + {file = "regex-2026.6.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a71b51dd08b9b62f055fafab3dee8af8bd2ec81b373a44caef18d6c5ca28f43a"}, + {file = "regex-2026.6.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9c26a47770d30a0f85c01e261d2a3ebc342c4af6fd666dbd8c1fe4cbf3adf726"}, + {file = "regex-2026.6.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e5efbc1af38f97e300d43028e5a92e752d924bcfb7f465d8669d5d5a6e78c233"}, + {file = "regex-2026.6.28-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1758df6fdd8c800620a5638958720e8a635e1da49a2f09df2dd63e94a24ec4a"}, + {file = "regex-2026.6.28-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ad73ecf20c1ef5c975639f8bf845a9370fcf7dada7edc1e3b0bca20e2f8202f6"}, + {file = "regex-2026.6.28-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4d80c798b0eec6ea3d45f8816a1e8886c5664615d347d89e8c075b576a1b5a5d"}, + {file = "regex-2026.6.28-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a361feeaf1b6ba1df060f2ff5c5947092edf537a35ce78e76387ac56d3e0f4a4"}, + {file = "regex-2026.6.28-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b92366d9c8bba9642989534073662abdd9b41faf7603a7ae71597833f3b88f0"}, + {file = "regex-2026.6.28-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:11251768cc23f097dd61b18f67966e70f74da822784d17e12a444eb6b29d4288"}, + {file = "regex-2026.6.28-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ad5c67786145ec28a71a267d9f9d92bdc8d70d65541eea852c253f520a01f918"}, + {file = "regex-2026.6.28-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f1da438e739765c3e85175ede05816cbede3caaacb1e0680568bda6119bfdfca"}, + {file = "regex-2026.6.28-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d98b639046e51c5de64d9f77351532105e99ca271cb6f7640e1f903d6ab63032"}, + {file = "regex-2026.6.28-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e164ace4dbab5c6ad4a4ac7c41a2638fe226d0c770a86f2eb041f594bac6ee7"}, + {file = "regex-2026.6.28-cp311-cp311-win32.whl", hash = "sha256:3169a3159e4d99d9ae85ff0ed90ef3b8906cc3152653b6078b842ace6c8f72c3"}, + {file = "regex-2026.6.28-cp311-cp311-win_amd64.whl", hash = "sha256:5977295b0a74e8241df8a4b3b27b12412a831f6fa32ee8b755039592cd768c3d"}, + {file = "regex-2026.6.28-cp311-cp311-win_arm64.whl", hash = "sha256:f5fbaef40c3e9282ccee4b075f5600a0d858aa0c34147732f1baa69c8188a95d"}, + {file = "regex-2026.6.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:81cc5793ad33a10444445e8d29d3c73e752c8fb2e120772d70fcb6d41df40fe1"}, + {file = "regex-2026.6.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e18225243250a1f7d7e5e5d883f3b96465cd79031acf5c6db902b7025f2125d9"}, + {file = "regex-2026.6.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ecd1638b1c2db1f2d01c182a4b0d3e2e88b0e99910320a745c1727ee3638ddab"}, + {file = "regex-2026.6.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4303ebe16b74eeb3fe2715745023266fea92fd44a23f3e7bb2fb48c7a7bbc195"}, + {file = "regex-2026.6.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56b856b70b96c381d837f609eee442a1bd320cd2159f5c294b679552fb1a7eaf"}, + {file = "regex-2026.6.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f74675ab76ab1d005ffba4dee308e53e89efc22be6e9f9fae5b539a3f81bdff2"}, + {file = "regex-2026.6.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90581684565a93f7258af1e5d3f41ef20d7d7c61f2a428183a342bcb65485e38"}, + {file = "regex-2026.6.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:28f9e6c28f9b90f6f784595a33240a57e181e61b6ee3dc259b25c61e356d1aa3"}, + {file = "regex-2026.6.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:378a71d861fc7c8806b04ac5b133d53c0e774f92f5d9663a539872d3fa2b0417"}, + {file = "regex-2026.6.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4cc199874ecd6267a49b111052250825bfe19b5101b23b2ba80f54efa3e0994e"}, + {file = "regex-2026.6.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b916a10431494ef4b4d62c6c89cab6426af7873125b8cd6c15811bf5fc58eec8"}, + {file = "regex-2026.6.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2e27727fba075f1e4409416d2f537d4c30fc11f012ea507f7bd74d3e19ecb57a"}, + {file = "regex-2026.6.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:700fc6a7844bb2c4149292ac79d1df8841a00acd4d45cd32c1ebc7bcc1fd0da8"}, + {file = "regex-2026.6.28-cp312-cp312-win32.whl", hash = "sha256:03376d60b6a11aecb88a79fa2be06b40faa01c6693bc31ef69435cd4818b9463"}, + {file = "regex-2026.6.28-cp312-cp312-win_amd64.whl", hash = "sha256:fbd2ded482bf99e6651992bbfcde460272724d4bbc49ef3d6b46d9312867ec84"}, + {file = "regex-2026.6.28-cp312-cp312-win_arm64.whl", hash = "sha256:37294d3d7ddb64c7e89184b2894e0f8f0a19c514bc59513d71fe692c3a8d5fc6"}, + {file = "regex-2026.6.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:b295a83426e0e44e9e60fde99789e181bd26788a1890ae7fe2a24c69bb6246ca"}, + {file = "regex-2026.6.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c31665c0deb5c111557a1cac8c27bd5629e2f9e7fd5058900a03576c33b601c"}, + {file = "regex-2026.6.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6bf295f2c59de77d1ea7de053607ae4dc9ceb3d57bbb6c7ec51ef4acc4ccff94"}, + {file = "regex-2026.6.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17c077586770f67e05bbffeba07fbee6b2b22244f4d4caf8d94e59d574befe04"}, + {file = "regex-2026.6.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0e6cb5a61486f9062397d2e189573b39d38ecfaed698fd9fb6e2756a8ebb8762"}, + {file = "regex-2026.6.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e86e91a2664f44c3a4e363a7d78fb17c27d5046882e30ea5a877f5e89b28d2ba"}, + {file = "regex-2026.6.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4dfd1331c49233998d84fc5f1f4436cf7a435a7655f6cf0f490229bb5c7254e5"}, + {file = "regex-2026.6.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cadea12805a1bce0b091c302b814207be26fb60a9c0e7f9ad2f9e21790a429fe"}, + {file = "regex-2026.6.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f2c1682b67ad5d2376498f2a5a2a8f782fa2e4a06d0465b5e357799806e8a20"}, + {file = "regex-2026.6.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:64e142eb55e84868087da1375d7c36ff97d55010951849f515322a91d5fef1b4"}, + {file = "regex-2026.6.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:abb4daabe7be63273787a62dfd6164dadf8f7a63fbec3d2730e5e5e7126d858c"}, + {file = "regex-2026.6.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec2b2ad00ab8c16a2798cc8db80c53c4d5b8b3a2441f6cbaef06625f5ca25854"}, + {file = "regex-2026.6.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bfc9677982c914d9085b8e1c3b3ae6e88f139fb56531c2416d6c8f338093c22b"}, + {file = "regex-2026.6.28-cp313-cp313-win32.whl", hash = "sha256:bf54bc693fc4e0530e666ba5ec4bcba14dbe8f66b7cfc15c27317d1a6e40b9a5"}, + {file = "regex-2026.6.28-cp313-cp313-win_amd64.whl", hash = "sha256:e128feaf65bf3d9eb91bec92322a8f7e4835e9c798f3e9ea4b69f4def85620e3"}, + {file = "regex-2026.6.28-cp313-cp313-win_arm64.whl", hash = "sha256:695873e0ea8d3815ea9e92e2c68faf039cc450e2c0a62a31afe2049eb11be767"}, + {file = "regex-2026.6.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:189dbf9fc4252d9f1352bf4bd1bef885edb6cc4b7341df202a65f821aaa3891c"}, + {file = "regex-2026.6.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9277a4c6503390aa39cb4483b87ec0384faee0850a23b5cea33d008b5d8d83f1"}, + {file = "regex-2026.6.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:17eddca4e8ea9af0b5739314776cdf0172a49731ab61f2e1ea66e066ddd46c97"}, + {file = "regex-2026.6.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4466b8641e00c697aab5a73150150d2b2ea96b131c595691f42031abafd9f4d"}, + {file = "regex-2026.6.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9cfcd4b0bdcf768c498415c170d1ed2a25a99bf0b65fa253bbd02f68ceba6475"}, + {file = "regex-2026.6.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:80c7adf1ef647f6b1e8aa2ca280e517174cd08bdf7a2e412cdfb68bd6a0917cb"}, + {file = "regex-2026.6.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a043f5770e82283a22aed4cefef1a4e0f9dd8fd7184cb6ce0ad2e579e2134a9e"}, + {file = "regex-2026.6.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3bd630a8dba06b55254ea5ee862194edab52ec783100d2ef1cd15a9c512fee27"}, + {file = "regex-2026.6.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b77207e3cee13086f1906a6a2a12b41244c577e8ad9370d4b35ae1d548d354f3"}, + {file = "regex-2026.6.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:6de82c268e5d101ee9e3ffd869924aa9a371e3a21e752cf4fa17b6ce50d219f7"}, + {file = "regex-2026.6.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b15859e3908544fb99cf47341dcf0bfd089147d258c4c4d8a29e5b087f8085cb"}, + {file = "regex-2026.6.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:c91487a917edd48a1ea646fdf60d7936d304f0e686fa7ea8326e47efca51d816"}, + {file = "regex-2026.6.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4ac65f3e3a99fd8f3a4a74e7a6610acd1ce9dfe9b8a03d346a4922380d68aeb"}, + {file = "regex-2026.6.28-cp313-cp313t-win32.whl", hash = "sha256:3f6316f258bc7e6c9c2acbe9954947bbd397a81be3742a637a555f1855d6618d"}, + {file = "regex-2026.6.28-cp313-cp313t-win_amd64.whl", hash = "sha256:1484bdd6fba28422df9b5ebb04055b2e1b680e8e4f08490bb21ff0f3cc50d0ab"}, + {file = "regex-2026.6.28-cp313-cp313t-win_arm64.whl", hash = "sha256:3f15020f0b69cafe57baa067ff65b29acef68ff6b1670a53bef1ca11d708e02d"}, + {file = "regex-2026.6.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:f7c032b0c8a73739ff8ff1aaf30c281fa19c17bf7f1543256c8507390db7807c"}, + {file = "regex-2026.6.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:f6710f512c57b84f127a23d0f59560a03b64136eff419ae1be5ab557577fe5e3"}, + {file = "regex-2026.6.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c0013958f427bd82509a186b9ff206d66cb8d60a81fc797a4c717afd18c5b0ba"}, + {file = "regex-2026.6.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f06cdcd6421f8e194ad312ea608020381250df9b8a57661c1b57e9e5273878"}, + {file = "regex-2026.6.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec9689392f7494ff4e3f8e7e8522f9158f11023f337eaaf04a64542fc45bbf26"}, + {file = "regex-2026.6.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aa084684e6d2078bf6139e374d1fc2af5ddc1ac7122759a2db716d68169f6fd0"}, + {file = "regex-2026.6.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40455e6840dc4e96a6fe50f4cedc957de2752c954d91e789812be55d49be199a"}, + {file = "regex-2026.6.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:530b5c223b9ca5dd8370ac502e080aee0e4ded32be987c6564b425fb5523d581"}, + {file = "regex-2026.6.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e0ed273ecd1a89be84466c1749bfe58609cc2a32b5d5e05006c4625ba96411b"}, + {file = "regex-2026.6.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0ab0d5344311fc8e8667078942056c3b9c9b4a4b1cc99f2eb8a5af54554f4acc"}, + {file = "regex-2026.6.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:eacb79625323d9f7e7925366b917f492b8356fad58f5dc4fa12ff8c21d8f4ca9"}, + {file = "regex-2026.6.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:20f4d87702702aa1d572721e146f301660c50eef6fd6cb596e48a22b0ace17db"}, + {file = "regex-2026.6.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1e693940a3b9e6d6e4dc2a54ecaa74b74934f77af1ef95f518a74261ef7cc1bc"}, + {file = "regex-2026.6.28-cp314-cp314-win32.whl", hash = "sha256:234a51e20ebc18ab83b2c0600cf28f2e884560a0e00f743878f0b7d8e7c4cf03"}, + {file = "regex-2026.6.28-cp314-cp314-win_amd64.whl", hash = "sha256:7b15c437bc4604f03ceb3f8d37eae2f8930e320e1bc556b259848c639d9eec1a"}, + {file = "regex-2026.6.28-cp314-cp314-win_arm64.whl", hash = "sha256:c6e6f790d01380a74ad564f216c533b86504afb61bf66f2b2e11e7f1a3e287a7"}, + {file = "regex-2026.6.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:3527a72adcbe9e3600f1553b497d397c1a371d227580d41d96c3c5964109b65c"}, + {file = "regex-2026.6.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a644f6408692812f5ead82519eed680e08d5d546fddbd9f7d9514e3c73899aa5"}, + {file = "regex-2026.6.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8e2fae6bb883648346f84db270dc9aafc29d8e895f62b88a75ccc83b09519820"}, + {file = "regex-2026.6.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:debe623e09cee97ef9404575e936c610aac9bb08358c5099aaef14644a6871f2"}, + {file = "regex-2026.6.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cc579c91fb4605773483a8d940b136bcc5b854fff44fa14a1572a038f46563f1"}, + {file = "regex-2026.6.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7c42be203d84ecf7d487ff23f8a61ef0eb0534fa0fc317a2fce8c065d20618f"}, + {file = "regex-2026.6.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8184b4e2fdaf9cdfe77e38f15a4d9dc149168c9c29eb0ea17c5481d3bb80546"}, + {file = "regex-2026.6.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:697f103104f5872d64078d8eeac59979960be8ee76115a2d3f31096312e2a400"}, + {file = "regex-2026.6.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:714d2b1aa29beef0ddfcdc72ad0771c05326551a8bb0680b0ddf74bfaad87387"}, + {file = "regex-2026.6.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0f09f62e450cc2f113018cc8412aeea3a120a04e1ca7e801a0d441583f9a3b06"}, + {file = "regex-2026.6.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:731ea12d5aeb2577eaef2393d6428b995f76eb35f68a89e03e15a97719d1de19"}, + {file = "regex-2026.6.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:51e952c8783eabd4706d0f63922f219bcfc1bef9b8cb35941c0d1a0396578858"}, + {file = "regex-2026.6.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:43248fe4c0ab8fbb223588a0795b11268940072c97bba30ea8f9b49d8cdfde34"}, + {file = "regex-2026.6.28-cp314-cp314t-win32.whl", hash = "sha256:fc1eddc25ad23c0f1344ab280d961ac595ead48292d7c779497975942373f493"}, + {file = "regex-2026.6.28-cp314-cp314t-win_amd64.whl", hash = "sha256:ede8d8e53b6dde0a50f7eca902f0af76d87ab02a55aba7542da68ae3e5dfe83d"}, + {file = "regex-2026.6.28-cp314-cp314t-win_arm64.whl", hash = "sha256:4da6f6a72f8700b97a1a765e837fb7d5750bfd9f13acea7bae498f573e3a70a8"}, + {file = "regex-2026.6.28.tar.gz", hash = "sha256:3cb4b6c5cb3060cc31efdc1fbb27c25fb9b29044afd87e40601a1c4d9db54342"}, ] [[package]] @@ -940,38 +883,15 @@ files = [ ] [[package]] -name = "sgmllib3k" -version = "1.0.0" -description = "Py3k port of sgmllib." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "sgmllib3k-1.0.0.tar.gz", hash = "sha256:7868fb1c8bfa764c1ac563d3cf369c381d1325d36124933a726f29fcdaa812e9"}, -] - -[[package]] -name = "soupsieve" -version = "2.8.4" -description = "A modern CSS selector implementation for Beautiful Soup." +name = "tzdata" +version = "2024.2" +description = "Provider of IANA time zone data" optional = false -python-versions = ">=3.9" +python-versions = ">=2" groups = ["main"] files = [ - {file = "soupsieve-2.8.4-py3-none-any.whl", hash = "sha256:e7e6b0769c8f51ed59acab6e994b00621096cfb1c640a7509295987388fbaf65"}, - {file = "soupsieve-2.8.4.tar.gz", hash = "sha256:e121fd02e975c695e4e9e8774a5ee35d74714b59307868dcc5319ad2d9e3328e"}, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -description = "Backported and Experimental Type Hints for Python 3.9+" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, ] [[package]] @@ -1006,21 +926,21 @@ zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "virtualenv" -version = "21.4.2" +version = "21.5.1" description = "Virtual Python Environment builder" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" groups = ["dev"] files = [ - {file = "virtualenv-21.4.2-py3-none-any.whl", hash = "sha256:854210ca524a1a4d0d744734f4acbc721c3ffe163b85bbf5d56d14d5ae2f0fae"}, - {file = "virtualenv-21.4.2.tar.gz", hash = "sha256:38e6ee0a555615c0ea9da2ac7e9998fe8dc3b911dd33ad8eaad2020957653b0c"}, + {file = "virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783"}, + {file = "virtualenv-21.5.1.tar.gz", hash = "sha256:dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = {version = ">=3.24.2,<4", markers = "python_version >= \"3.10\""} platformdirs = ">=3.9.1,<5" -python-discovery = ">=1.4" +python-discovery = ">=1.4.2" [[package]] name = "win32-setctime" @@ -1153,4 +1073,4 @@ cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and pyt [metadata] lock-version = "2.1" python-versions = ">=3.11,<3.15" -content-hash = "9fa7378a8df2620a9ee54a7b989622a809fabff10605969a57e54021d73d671c" +content-hash = "98215fe1e749b03338f96fc7994319c71b530765c4d0711672f2e83c25f163db" diff --git a/pyproject.toml b/pyproject.toml index 4e4f20ee..5a01a54e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,9 +14,8 @@ mwclient="^0.11.0" keyvalues3="^0.1" python-mermaid="^0.1.3" python-dotenv = "^1.0.1" -feedparser = "^6.0.11" requests = "^2.32.3" -bs4 = "^0.0.2" +tzdata = "^2024.2" loguru = "^0.7.2" zstandard = "^0.25.0" pillow = "^12.2.0" diff --git a/src/changelogs/constants.py b/src/changelogs/constants.py index 57c1a2ae..82fc30b7 100644 --- a/src/changelogs/constants.py +++ b/src/changelogs/constants.py @@ -1 +1,5 @@ -CHANGELOG_RSS_URL = 'https://forums.playdeadlock.com/forums/changelog.10/index.rss' +STEAM_APP_ID = '1422450' +STEAM_NEWS_API_URL = ( + f'https://api.steampowered.com/ISteamNews/GetNewsForApp/v2/?appid={STEAM_APP_ID}&count=50&maxlength=0&feeds=steam_community_announcements' +) +STEAM_MIGRATION_DATE = '2026-06-04' diff --git a/src/changelogs/fetch_changelogs.py b/src/changelogs/fetch_changelogs.py index 0899c1c7..6a43b96a 100644 --- a/src/changelogs/fetch_changelogs.py +++ b/src/changelogs/fetch_changelogs.py @@ -1,14 +1,10 @@ import os -from os import listdir -from os.path import isfile, join from loguru import logger -import feedparser -from bs4 import BeautifulSoup -from urllib import request -from urllib.parse import urlparse +import requests +import hashlib from utils import file_utils, json_utils -from typing import TypedDict -from .constants import CHANGELOG_RSS_URL +from typing import TypedDict, NotRequired +from .constants import STEAM_APP_ID, STEAM_NEWS_API_URL, STEAM_MIGRATION_DATE from collections import defaultdict from datetime import datetime from zoneinfo import ZoneInfo @@ -19,20 +15,25 @@ class ChangelogConfig(TypedDict): """ Each record in changelog_configs.json - Key is "changelog_id", default to forum_id, differs for herolab changelogs + Key is "changelog_id", default to source_id, differs for herolab changelogs """ - forum_id: str + source_id: str # Now stores the Steam GID date: str link: str + is_hero_lab: bool + title: NotRequired[str] + steam_hash: NotRequired[str] class ForumUpdate(TypedDict): - """Represents a single update entry fetched from the forum""" + """Represents a single update entry fetched from the Steam API""" version: str text: str link: str + title: NotRequired[str] + steam_hash: NotRequired[str] class Hotfix(TypedDict): @@ -50,10 +51,10 @@ class ChangelogString(TypedDict): class ChangelogFetcher: """ - Fetches changelogs from the deadlock forums and game files and parses them into a dictionary + Fetches changelogs from Steam Web API and game files and parses them into a dictionary """ - def __init__(self, update_existing, input_dir, output_dir, herolab_patch_notes_path): + def __init__(self, update_existing, input_dir, output_dir): self.changelogs: dict[str, ChangelogString] = {} self.changelog_configs: dict[str, ChangelogConfig] = {} self.hotfixes: list[Hotfix] = [] @@ -62,15 +63,8 @@ def __init__(self, update_existing, input_dir, output_dir, herolab_patch_notes_p self.INPUT_DIR = input_dir self.OUTPUT_DIR = output_dir - self.RSS_URL = CHANGELOG_RSS_URL - - # Safely derive base url (e.g., https://forums.playdeadlock.com) - parsed_url = urlparse(self.RSS_URL) - self.FORUM_BASE_URL = f'{parsed_url.scheme}://{parsed_url.netloc}' - - self.HEROLABS_PATCH_NOTES_PATH = herolab_patch_notes_path - - self.TAGS_TO_REMOVE = ['', '', '', '', ''] + self.STEAM_NEWS_URL = STEAM_NEWS_API_URL + self.APP_ID = STEAM_APP_ID self.wiki_site = None @@ -143,33 +137,54 @@ def _get_patch_section(self, header: str, full_text: str) -> str: Extract a specific patch section from changelog text. Args: - header: Patch header like "=== Patch 2 ===" + header: Patch header like "== Patch 2 ==" full_text: Complete changelog text Returns: str: The patch section including header, or empty string if not found """ - pattern = re.compile(rf'({re.escape(header)})(.*?)(?=(=== Patch \d+ ===)|$)', re.DOTALL) + pattern = re.compile(rf'({re.escape(header)})(.*?)(?=(== Patch \d+ ==)|$)', re.DOTALL) match = pattern.search(full_text) return match.group(0).strip() if match else '' - def run(self): - self.load_localization() - self.fetch_forum_changelogs() - self.get_gamefile_changelogs() - self.changelogs_to_file() + def _bbcode_to_text(self, bbcode: str) -> str: + """Converts Steam BBCode to plain text matching existing parser expectations.""" + text = bbcode.replace('\\[', '[').replace('\\]', ']') + text = text.replace('[p]', '').replace('[/p]', '\n') + + # Handle lists explicitly before the catch-all + text = re.sub(r'\[/?list\]', '', text) + text = re.sub(r'\[\*\]', '- ', text) + + # Remove bold/italic/underline tags + text = re.sub(r'\[/?b\]', '', text) + text = re.sub(r'\[/?i\]', '', text) + text = re.sub(r'\[/?u\]', '', text) + + # Handle links: [url=...]text[/url] -> text + text = re.sub(r'\[url=[^\]]*\](.*?)\[/url\]', r'\1', text) + text = re.sub(r'\[/?url\]', '', text) + + # Handle images, steam items, youtube + text = re.sub(r'\[img[^\]]*\].*?\[/img\]', '', text, flags=re.DOTALL) + text = re.sub(r'\[steamitem[^\]]*\].*?\[/steamitem\]', '', text, flags=re.DOTALL) + text = re.sub(r'\[previewyoutube[^\]]*\].*?\[/previewyoutube\]', '', text, flags=re.DOTALL) - def load_localization(self): - self.localization_data_en = json_utils.read(os.path.join(self.OUTPUT_DIR, 'localizations', 'english.json')) + # Catch-all: remove any remaining BBCode tags we missed + text = re.sub(r'\[/?[a-zA-Z0-9]+(?:\s[^\]]*)?\]', '', text) - def get_txt(self, changelog_path): - self._process_local_changelogs(changelog_path) - return self.changelogs + # Clean up extra blank lines + text = re.sub(r'\n{3,}', '\n\n', text).strip() + return text + + def run(self): + self.fetch_steam_changelogs() + self.changelogs_to_file() def changelogs_to_file(self): """ Save combined changelogs to input and output directories. - Saving to input directory is necessary as the RSS feed can only see + Saving to input directory is necessary as the Steam API only provides recent changelogs. Since output data is paved over each deploy, we need this source for historic @@ -197,257 +212,85 @@ def changelogs_to_file(self): # Save decoupled hotfixes for the uploader json_utils.write(f'{self.OUTPUT_DIR}/changelogs/hotfixes.json', self.hotfixes) - def _fetch_update_html(self, link): - """ - Fetches the HTML content of a forum link and extracts posts grouped by date. - Returns: - dict: { 'YYYY-MM-DD': [ {'text': str, 'link': str}, ... ] } - """ - html = request.urlopen(link).read() - soup = BeautifulSoup(html, features='html.parser') + def fetch_steam_changelogs(self): + """Fetch patch notes from Steam Web API.""" + logger.trace('Fetching Steam news for changelogs') + target_tz = ZoneInfo('US/Pacific') - # Dictionary to group posts by date string 'YYYY-MM-DD' - content_by_date = defaultdict(list) + try: + resp = requests.get(self.STEAM_NEWS_URL, timeout=30) + resp.raise_for_status() + data = resp.json() + except Exception as e: + logger.error(f'Failed to fetch Steam news: {e}') + return - # XenForo posts are wrapped in
- articles = soup.find_all('article', class_='message') + news_items = data.get('appnews', {}).get('newsitems', []) + updates_by_day: dict[str, list[ForumUpdate]] = defaultdict(list) - # Target Timezone: Valve HQ (US/Pacific) - target_tz = ZoneInfo('US/Pacific') + for item in news_items: + try: + tags = item.get('tags', []) + if 'patchnotes' not in tags: + logger.trace(f"Skipping non-patch-note news: {item.get('title')} (Tags: {tags})") + continue - for i, article in enumerate(articles): - # Extract the Date - time_elem = article.find('time', class_='u-dt') - if not time_elem or not time_elem.has_attr('datetime'): - continue + gid = str(item['gid']) + url = item.get('url') or f'https://store.steampowered.com/news/app/{self.APP_ID}/view/{gid}' + title = item.get('title', '') + date_unix = item['date'] + contents_bbcode = item.get('contents', '') - try: - dt_str = time_elem['datetime'] - # Parse ISO string (e.g. 2024-11-21T01:00:00+0000) - dt_utc = datetime.fromisoformat(dt_str) - # Convert to Valve HQ time + dt_utc = datetime.fromtimestamp(date_unix, tz=ZoneInfo('UTC')) dt_valve = dt_utc.astimezone(target_tz) post_date = dt_valve.strftime('%Y-%m-%d') - except (IndexError, KeyError, ValueError) as e: - logger.warning(f'Failed to parse date from post: {e}') - continue - - # Extract the Link - if i == 0: - # The first post found is the Main Thread. - # Use the canonical link passed into the function (e.g. /threads/update.123/) - post_link = link - else: - # For subsequent posts (comments/hotfixes), find the specific permalink - post_link = None - attribution = article.find('ul', class_='message-attribution-main') - if attribution: - a_tag = attribution.find('a', href=True) - if a_tag: - href = a_tag['href'] - if href.startswith('http'): - post_link = href - else: - # Join base and relative path ensuring exactly one slash - post_link = self.FORUM_BASE_URL.rstrip('/') + '/' + href.lstrip('/') - - # Fallback if extraction fails - if not post_link: - post_link = link - - # Extract the Content - content_div = article.find('div', class_='bbWrapper') - if not content_div: - continue - - text = content_div.text.strip() - if text: - content_by_date[post_date].append({'text': text, 'link': post_link}) - - return content_by_date - - def get_gamefile_changelogs(self): - """Read and parse the Hero Labs changelogs in the game files""" - changelogs = json_utils.read(self.HEROLABS_PATCH_NOTES_PATH) - - gamefile_changelogs = dict() - # Iterate each pair in the patch note localization - for key, string in changelogs.items(): - # Not a real patch note - if key == 'Language': - continue - - # key i.e. Citadel_PatchNotes_HeroLabs_hero_astro_1 - # string i.e. "10/24/2024\t\t\t - #
  • Hero Added to Hero Labs
  • - # Changed y from x to z
  • " - - # Parse the date from the beginning of the string - date_raw = string.split('\t')[0].replace('', '').replace('', '') - if len(date_raw) > 10: - date_raw = date_raw[:10] - - # Remove date from remaining string - remaining_str = string.replace(f'{date_raw}\t\t\t', '') - - # Reformat mm/dd/yyyy to yyyy-mm-dd - date = format_date(date_raw) - - # Parse hero name to create a header for the changelog entry - # Citadel_PatchNotes_HeroLabs_hero_astro_1 -> - # hero_astro -> - # Holliday -> - # [ HeroLab Holliday ] - hero_key = key.split('Citadel_PatchNotes_HeroLabs_')[1][:-2] - hero_name_en = self._localize(hero_key) - header = f'[ HeroLab {hero_name_en} ]' - - # Initialize the changelog entry if its the first line for this hero's patch (version) - # Create the raw changelog id (used as filename in raw folder) - # i.e. 2024-10-29_HeroLab - raw_changelog_id = f'{date.replace("/", "-")}_HeroLab' - if raw_changelog_id not in gamefile_changelogs: - gamefile_changelogs[raw_changelog_id] = header + '\n' - else: - gamefile_changelogs[raw_changelog_id] += '\n' + header + '\n' - - # Ensure the date was able to be removed and was in the correct format - if len(remaining_str) == len(string): - logger.warning(f'Date format may not have been able to be parsed correctly for {date}') - - # Parse full description by accumulating each description separated by
  • tags - #
  • Text 1
  • Text 2
  • -> Text 1\nText 2\n - while len(remaining_str) > 0: - # Parse the next description - description, remaining_str = self._parse_description(remaining_str) - if description is None: - break - - # Add to the current changelog entry - gamefile_changelogs[raw_changelog_id] += f'- {description}\n' - - # Add the config entry if it doesn't exist - if raw_changelog_id not in self.changelog_configs: - self.changelog_configs[raw_changelog_id] = { - 'forum_id': None, - 'date': date, - 'link': None, - 'is_hero_lab': True, - } - self.changelogs.update(gamefile_changelogs) - - def _parse_description(self, string): - # Find the next
  • tag - li_start, li_end = self._find_li_tags(string) - if li_start == -1: - # No more list elements to be found - return None, string - if li_end == -1: - raise ValueError(f'No closing
  • tag found in description {string}') - - # Extract the description - description = string[li_start + len('
  • ') : li_end] - - # Remove the description from the remaining string - string = string[li_end + len('
  • ') :] - - # Remove any remaining tags that are unneeded - for tag in self.TAGS_TO_REMOVE: - description = description.replace(tag, '') - - return description, string - - def _find_li_tags(self, string): - # Find the next
  • tag - li_start = string.find('
  • ') - - # If no list elements are found, return right away - if li_start == -1: - return li_start, -1 - - # Find the next li tag, which will either be
  • , or
  • - li_end = string.find('
  • ', li_start + len('
  • ')) - # if no more
  • 's, find the last
  • - if li_end == -1: - li_end = string.find('', li_start + len('
  • ')) - - return li_start, li_end - - def _localize(self, key): - value = self.localization_data_en.get(key, None) - if value is None: - raise Exception(f'Localized string not found for key {key}') - return value - - def fetch_forum_changelogs(self): - """download rss feed from changelog forum and save all available entries""" - logger.trace('Parsing Changelog RSS feed') - # fetches 20 most recent entries - feed = feedparser.parse(self.RSS_URL) - - # Bucket to collect all updates grouped by date - updates_by_day: dict[str, list[ForumUpdate]] = defaultdict(list) - - for entry in feed.entries: - # Parse version ID from URL - version = entry.link.split('.')[-1].split('/')[0] - try: - int(version) - except ValueError: - try: - version = entry.link.split('/')[-2].split('.')[-1] - int(version) - except ValueError: + # Only process patch notes on or after 2026-06-04. + # Before this date, changelogs were sourced from the Deadlock forums and + # already exist in the data directory. Overwriting them with Steam-sourced + # content would replace forum URLs and formatting, which is undesirable. + if post_date < STEAM_MIGRATION_DATE: + logger.trace(f'Skipping old patch note before 2026-06-04: {title}') continue - if version is None or version == '': - continue + logger.trace(f'Processing Steam patch note: {title} ({post_date})') + text = self._bbcode_to_text(contents_bbcode) + steam_hash = hashlib.md5(contents_bbcode.encode('utf-8')).hexdigest() - try: - # Scrape this thread for date-grouped posts - thread_updates = self._fetch_update_html(entry.link) + updates_by_day[post_date].append({'version': gid, 'text': text, 'link': url, 'title': title, 'steam_hash': steam_hash}) except Exception as e: - logger.error(f'Issue with parsing RSS feed item {entry.link}: {e}') + logger.error(f'Failed to parse Steam news item: {e}') continue - # Add these updates to our master bucket - for date_key, posts in thread_updates.items(): - # Join text if multiple posts exist within ONE thread for ONE day - text_parts = [] - for i, p in enumerate(posts): - content = p['text'] - if i > 0: - content = f'=== Patch {i + 1} ===\n{content}' - text_parts.append(content) - - full_text = '\n\n'.join(text_parts) - specific_link = posts[0]['link'] - - updates_by_day[date_key].append({'version': version, 'text': full_text, 'link': specific_link}) - - # Now process the gathered updates day by day for date_key, entries in updates_by_day.items(): changelog_id = date_key - # Check for local file first (already processed by bot) local_path = os.path.join(self.INPUT_DIR, 'changelogs/raw', f'{changelog_id}.txt') local_content = file_utils.read(local_path) if os.path.exists(local_path) else '' - # Check for wiki page (manual pages created by editors) wiki_content = self._get_wiki_content(changelog_id) if not local_content else '' existing_config = self.changelog_configs.get(changelog_id) main_entry = None append_entries = [] + was_edited = False - # Find the main entry if existing_config: + matched = False for e in entries: - if e['version'] == existing_config['forum_id']: + if e['version'] == existing_config.get('source_id'): main_entry = e + matched = True + if existing_config.get('steam_hash') and existing_config['steam_hash'] != e['steam_hash']: + logger.warning(f'Steam post for {date_key} was edited by the devs! Updating local file.') + local_content = '' + was_edited = True break + if not matched and entries: + entries.sort(key=lambda x: x['version']) + main_entry = entries[0] + for e in entries: if e != main_entry: append_entries.append(e) @@ -462,20 +305,18 @@ def fetch_forum_changelogs(self): # (will still check for hotfixes to append to wiki later) current_text = local_content elif wiki_content and main_entry: - # Wiki page exists but no local file - append forum content to wiki - logger.info(f'Found wiki page for {date_key} without local file, appending forum content') + # Wiki page exists but no local file - append new content to wiki + logger.info(f'Found wiki page for {date_key} without local file, appending new content') append_entries.insert(0, main_entry) current_text = wiki_content main_entry = None else: - # No existing content - use forum as base + # No existing content - use fetched content as base current_text = main_entry['text'] if main_entry else (self.changelogs.get(changelog_id, '')) - old_text = local_content or wiki_content or '' - # Helper to get next patch number def get_next_patch_num(txt): - matches = re.findall(r'=== Patch (\d+) ===', txt) + matches = re.findall(r'== Patch (\d+) ==', txt) if matches: return max(map(int, matches)) + 1 # If there's existing content but no patches yet, this will be Patch 2 @@ -486,18 +327,25 @@ def get_next_patch_num(txt): for entry in append_entries: # Remove any existing patch headers from the entry first entry_text = entry['text'] - entry_text = re.sub(r'=== Patch \d+ ===\n*', '', entry_text).strip() + entry_text = re.sub(r'== Patch \d+ ==\n*', '', entry_text).strip() # Skip if this content is already in final_text normalized_entry = re.sub(r'^- ', '* ', entry_text, flags=re.MULTILINE) normalized_final = re.sub(r'^- ', '* ', final_text, flags=re.MULTILINE) if normalized_entry not in normalized_final: patch_num = get_next_patch_num(final_text) - final_text += f'\n\n=== Patch {patch_num} ===\n\n{entry_text}' + final_text += f'\n\n== Patch {patch_num} ==\n\n{entry_text}' logger.debug(f'Adding Patch {patch_num} to {date_key}') + # If we overwrote the local file due to an edit, prevent false hotfix detection + # by considering the newly generated text as the "old text". + if was_edited: + old_text = final_text + else: + old_text = local_content or wiki_content or '' + # Detect hotfixes - compare new patches against old content - potential_headers = re.findall(r'=== Patch \d+ ===', final_text) + potential_headers = re.findall(r'== Patch \d+ ==', final_text) for h in potential_headers: if h not in old_text: section_text = self._get_patch_section(h, final_text) @@ -508,99 +356,21 @@ def get_next_patch_num(txt): # Save to memory and update config self.changelogs[changelog_id] = final_text - # Determine forum_id with priority: main_entry > existing_config > append_entries - if main_entry: - forum_id = main_entry['version'] - elif existing_config: - forum_id = existing_config.get('forum_id') - elif append_entries: - forum_id = append_entries[0]['version'] - else: - forum_id = None - - # Determine link with same priority - if main_entry: - link = main_entry['link'] - elif existing_config: - link = existing_config.get('link') - elif append_entries: - link = append_entries[0]['link'] - else: - link = None + # Determine source_id, link, title, and steam_hash with priority: + # main_entry > existing_config > append_entries + source_data = main_entry or existing_config or (append_entries[0] if append_entries else {}) + + # main_entry and append_entries use 'version', existing_config uses 'source_id' + source_id = source_data.get('version') or source_data.get('source_id') + link = source_data.get('link') + title = source_data.get('title') + steam_hash = source_data.get('steam_hash') self.changelog_configs[changelog_id] = { - 'forum_id': forum_id, + 'source_id': source_id, 'date': date_key, 'link': link, 'is_hero_lab': False, + 'title': title, + 'steam_hash': steam_hash, } - - def _process_local_changelogs(self, changelog_path): - logger.trace('Parsing Changelog txt files') - # Make sure path exists - if not os.path.isdir(changelog_path): - logger.warning(f'Issue opening changelog dir `{changelog_path}`') - return - files = [f for f in listdir(changelog_path) if isfile(join(changelog_path, f))] - logger.trace(f'Found {str(len(files))} changelog entries in `{changelog_path}`') - for file in files: - version = file.replace('.txt', '') - try: - with open(changelog_path + f'/{version}.txt', 'r', encoding='utf8') as f: - changelogs = f.read() - self.changelogs[version] = changelogs - except Exception: - logger.warning(f'Issue with {file}, skipping') - - def _create_changelog_id(self, date, forum_id, i=0): - """ - Creating a custom id based on the date by appending _ - if its another patch for the same day, i.e.: - 2024-10-29 - 2024-10-29-1 - 2024-10-29-2 - """ - - # (2024-12-17, 0) -> 2024-12-17 - # (2024-12-17, 1) -> 2024-12-17-1 - id = date if i == 0 else f'{date}-{i}' - - # Existing config for this date - existing_config = self.changelog_configs.get(id, None) - - # If this id doesn't yet exist, use it - if existing_config is None: - return id - # Else same date already exists - - # If the forum id is the same, use the same changelog id - # which will update the existing record - if existing_config['forum_id'] == forum_id: - return id - # Else forum id's are different, so different patches on the same day - - # Recursively check if the next id is available - return self._create_changelog_id(date, forum_id, i + 1) - - -def format_date(date): - """ - Reformat mm/dd/yyyy or mm-dd-yyyy to yyyy-mm-dd - Also convert days and months to 2 digits - """ - # Split date by / or - - if '/' in date: - date = date.split('/') - elif '-' in date: - date = date.split('-') - else: - raise ValueError(f'Invalid date format {date}') - - # If the day or month is a single digit, add a leading 0 - for i in range(2): - if len(date[i]) == 1: - date[i] = '0' + date[i] - - # Reformat to yyyy-mm-dd - date = f'{date[2]}-{date[0]}-{date[1]}' - return date diff --git a/src/changelogs/link_targets.json b/src/changelogs/link_targets.json index 7c51bb77..22a06219 100644 --- a/src/changelogs/link_targets.json +++ b/src/changelogs/link_targets.json @@ -25,5 +25,7 @@ "Base Guardian": ["Base Guardian"], "Parry": ["Parry", "parry"], "Hidden King": ["Hidden King"], - "Archmother": ["Archmother"] + "Archmother": ["Archmother"], + "Unstable Rift": ["Unstable Rift"], + "Rift Trooper": ["Rift Trooper", "Rift Troopers", "rift trooper", "rift troopers"] } diff --git a/src/changelogs/parse_changelogs.py b/src/changelogs/parse_changelogs.py index 5478a044..07deb6a3 100644 --- a/src/changelogs/parse_changelogs.py +++ b/src/changelogs/parse_changelogs.py @@ -6,6 +6,7 @@ from utils import file_utils from .tags import ChangelogTags as Tags from . import wikitext_formatter +from .constants import STEAM_MIGRATION_DATE class ChangelogParser: @@ -101,7 +102,7 @@ def format_and_save_wikitext_changelogs(self, changelogs, changelog_configs): for changelog_id, raw_text in changelogs.items(): config = changelog_configs.get(changelog_id) - # Skip if config is missing or if it's a Hero Lab entry. + # Hero Lab entries no longer exist in the game and don't get updates, skip them. if not config or config.get('is_hero_lab'): continue @@ -110,8 +111,17 @@ def format_and_save_wikitext_changelogs(self, changelogs, changelog_configs): logger.warning(f"Changelog '{changelog_id}' is missing a date. Skipping wiki page creation.") continue + # Skip historical forum changelogs. They are already on the wiki + # and don't need to be re-generated or re-uploaded. + if changelog_date < STEAM_MIGRATION_DATE: + continue + formatted_body = wikitext_formatter.format_changelog(raw_text, hero_data, item_data, ability_data, link_targets=link_targets) + # If the formatted text doesn't start with a bullet point, there are no general changes. + # We set hide_general=true so the wiki template doesn't render an empty '== General ==' section. + hide_general = 'true' if not formatted_body.lstrip().startswith('*') else '' + date_obj = datetime.strptime(changelog_date, '%Y-%m-%d') # Calculate Previous Link using {{Update link}} @@ -129,27 +139,8 @@ def format_and_save_wikitext_changelogs(self, changelogs, changelog_configs): except ValueError: pass - source_link = config.get('link', '') - source_title = '' - if source_link: - # Logic to extract title from URL - parts = source_link.split('/') - slug = '' - for part in parts: - if '-update' in part: - slug = part - break - if not slug and len(parts) >= 2: - slug = parts[-2] if parts[-1] == '' else parts[-1] - - title_part = slug.split('.')[0] - source_title = title_part.replace('-update', ' Update') - - # Check for reply indicators in the URL - if '/post-' in source_link or '/posts/' in source_link: - source_title += ' (Reply)' - else: - source_title = f"{date_obj.strftime('%m-%d-%Y')} Update" + source_link = config.get('link') or '' + source_title = config.get('title') or f"{date_obj.strftime('%m-%d-%Y')} Update" full_page_content = f"""{{{{Update layout | prev_update = {prev_update_link} @@ -159,8 +150,9 @@ def format_and_save_wikitext_changelogs(self, changelogs, changelog_configs): | next_update = | source = {source_link} | source_title = {source_title} +| hide_general = {hide_general} | notes = -{formatted_body} +{formatted_body.rstrip()} }}}}""" file_utils.write(os.path.join(output_path, f'{changelog_id}.txt'), full_page_content) @@ -228,13 +220,6 @@ def _parse_tags(self, current_heading, line): if heading_tag is not None: tags = self._register_tag(tags, tag=heading_tag) - # If the heading is a "HeroLab ", register as well - if current_heading.startswith('HeroLab '): - hero = current_heading[len('HeroLab ') :] - if self.is_hero(hero): - tags = self._register_tag(tags, hero) - tags = self._register_tag(tags, 'Hero') - # if no tag is found, assign to default tag if len(tags) == 0: tags = self._register_tag(tags, tag=self.default_tag) @@ -370,17 +355,6 @@ def _get_resources(self): return resources - def is_hero(self, tag): - """ - Returns True if the tag is a hero name i.e. Abrams - """ - for hero_key, hero_data in self.heroes.items(): - hero_name = hero_data['Name'] - if tag == hero_name: - return True - - return False - # Given an ability key, return the first hero that has that ability def get_hero_from_ability(self, ability_key_to_search): for hero_key in self.heroes: diff --git a/src/changelogs/tags.py b/src/changelogs/tags.py index 5cabaa89..66be9e8c 100644 --- a/src/changelogs/tags.py +++ b/src/changelogs/tags.py @@ -373,7 +373,6 @@ def _write_tag_tree(self, tag_tree): # Add , , etc. to tag tree # to show where instance tags would appear (such as Abrams, Basic Magazine, Siphon Life) tag_tree['Hero'][''] = {} - tag_tree['Hero'][''] = {'HeroLab ': {}} tag_tree['Hero']['Ability'][''] = {} tag_tree['Item']['Weapon Item'][''] = {} tag_tree['Item']['Vitality Item'][''] = {} diff --git a/src/changelogs/wikitext_formatter.py b/src/changelogs/wikitext_formatter.py index d13f08b7..00ace4c1 100644 --- a/src/changelogs/wikitext_formatter.py +++ b/src/changelogs/wikitext_formatter.py @@ -1,5 +1,13 @@ import re -from typing import Dict, Any, Optional +from typing import Any, Dict, List, Optional, Tuple + + +# Game mode names that should produce their own section rather than being classified as items or heroes. +GAME_MODE_SECTIONS = ['Street Brawl'] + +# fetch_changelogs.py inserts "=== Patch N ===" headers when merging same-day Steam posts. +# This regex segments on those headers so each patch block is grouped independently. +_PATCH_HEADER_RE = re.compile(r'^==\s*Patch\s+\d+\s*==$', re.MULTILINE) def format_changelog( @@ -10,84 +18,315 @@ def format_changelog( link_targets: Optional[Dict[str, list]] = None, ) -> str: """ - Formats a raw changelog string into wikitext by replacing entity names - with icon templates and converting markdown-style bullet points. + Formats a raw changelog string into structured wikitext. + Categorizes entries into Game Modes, Items, and Heroes/Abilities, + and replaces entity names with icon templates. Args: - raw_text (str): The raw text of the changelog. - hero_data (dict): Parsed hero data from hero-data.json. - item_data (dict): Parsed item data from item-data.json. - ability_data (dict): Parsed ability data from ability-data.json. - link_targets (dict): Map of page_name -> list of aliases for auto-linking common terms. + raw_text: The raw text of the changelog. + hero_data: Parsed hero data from hero-data.json. + item_data: Parsed item data from item-data.json. + ability_data: Parsed ability data from ability-data.json. + link_targets: Map of page_name -> list of aliases for auto-linking common terms. Returns: - str: The formatted wikitext string. + The formatted wikitext string. """ if not raw_text: return '' - # Create a mapping from entity names to their wikitext templates. - entity_to_template = {} + heroes, items, abilities = _collect_names(hero_data, item_data, ability_data) + + parts = _PATCH_HEADER_RE.split(raw_text) + headers = _PATCH_HEADER_RE.findall(raw_text) + segments = [(None, parts[0])] + list(zip(headers, parts[1:])) + + output_blocks = [] + for header, body_text in segments: + entries = _parse_entries(body_text) + section_text = _format_entries(entries, heroes, items, abilities, link_targets) + + if header: + output_blocks.append(header.strip()) + if section_text: + output_blocks.append(section_text) + + if not output_blocks: + return '' + + return '\n\n'.join(output_blocks).strip() + '\n' - # 1. High Priority: Game Entities (Heroes, Abilities, Items) get Icons - data_sources = [ - (hero_data, 'HeroIcon'), - (ability_data, 'AbilityIcon'), - (item_data, 'ItemIcon'), - ] - for data, template_name in data_sources: - if not data: +def _format_entries( + entries: List[Tuple[bool, str]], + heroes: List[str], + items: List[str], + abilities: List[str], + link_targets: Optional[Dict[str, list]], +) -> str: + """ + Parses categorized entries (bullets and raw text) into a structured hierarchy. + Each entry is a tuple of ``(is_bullet, text)``. Bullet entries are classified into + Game Modes, Items, or Heroes/Abilities; raw text goes to the general section. + Builds nested dictionaries for Game Modes, Items, and Heroes (with sub-sections for abilities), + then passes the formatted text to _build_output to generate the final wikitext layout. + """ + general: List[str] = [] + modes: Dict[str, List[str]] = {} + mode_order: List[str] = [] + item_sections: Dict[str, List[str]] = {} + item_order: List[str] = [] + hero_sections: Dict[str, Dict[str, Any]] = {} + hero_order: List[str] = [] + + for is_bullet, entry in entries: + if not is_bullet: + # Raw text (like "Primary details:") goes to general without a bullet + formatted = _format_body(entry, heroes, abilities, items, link_targets) + general.append(formatted) continue - for entry in data.values(): - name = entry.get('Name') - # Only add active (not disabled) entities to the template map. - is_disabled = entry.get('IsDisabled', False) - if name and not is_disabled: - entity_to_template[name] = f'{{{{{template_name}|{name}}}}}' - - # 2. Medium Priority: Curated Wiki Links (from include list) - # We add these after Icons. Since we use `get` later, existing keys (Icons) won't be overwritten. + + kind, name, body = _classify(entry, heroes, items, GAME_MODE_SECTIONS) + + if kind == 'general': + general.append(f'* {_format_body(body, heroes, abilities, items, link_targets)}') + + elif kind == 'mode': + if name not in modes: + modes[name] = [] + mode_order.append(name) + modes[name].append(f'* {_format_body(body, heroes, abilities, items, link_targets)}') + + elif kind == 'item': + if name not in item_sections: + item_sections[name] = [] + item_order.append(name) + formatted = _format_body(body, heroes, abilities, items, link_targets) + item_sections[name].append(f'* {{{{Change|}}}} {formatted}') + + elif kind == 'hero': + if name not in hero_sections: + hero_sections[name] = {'general': [], 'ability_sections': {}, 'ability_order': []} + hero_order.append(name) + + section = hero_sections[name] + mention = _find_ability_mention(body, abilities) + + if mention: + start, end, ability = mention + cleaned = _remove_ability_mention(body, start, end) + else: + cleaned, ability = None, None + + if mention and cleaned: + if ability not in section['ability_sections']: + section['ability_sections'][ability] = [] + section['ability_order'].append(ability) + + formatted = _format_body(_capitalize_first(cleaned), heroes, abilities, items, link_targets, excluded_ability=ability) + section['ability_sections'][ability].append(f'* {{{{Change|}}}} {formatted}') + else: + formatted = _format_body(body, heroes, abilities, items, link_targets) + section['general'].append(f'* {{{{Change|}}}} {formatted}') + + return _build_output(general, modes, mode_order, item_sections, item_order, hero_sections, hero_order) + + +def _collect_names(hero_data, item_data, ability_data) -> Tuple[List[str], List[str], List[str]]: + return _active_names(hero_data), _active_names(item_data), _active_names(ability_data) + + +def _active_names(data: Dict[str, Any]) -> List[str]: + if not data: + return [] + return [entry['Name'] for entry in data.values() if entry.get('Name') and not entry.get('IsDisabled', False)] + + +def _parse_entries(raw_text: str) -> List[Tuple[bool, str]]: + """ + Splits raw text into individual entries. + Returns a list of tuples: (True, text) for bullets, (False, text) for raw prose. + Bracket headings like '[ Items ]' are dropped to prevent corruption. + """ + entries: List[Tuple[bool, str]] = [] + + for line in raw_text.split('\n'): + stripped = line.strip() + if not stripped: + continue + + if stripped.startswith('-'): + entries.append((True, stripped[1:].strip())) + else: + # Drop bracket headings like [ Items ] or [ General ] + if re.match(r'^\[.*\]$', stripped): + continue + # Keep other lines (like "Primary details:") as raw text + entries.append((False, re.sub(r'\s+', ' ', stripped).strip())) + + return entries + + +def _match_hero_prefix(raw_prefix: str, heroes: List[str]) -> Optional[str]: + """ + Returns the canonical hero name matching *raw_prefix*, or ``None``. + + Patch notes sometimes refer to a hero by its sort name (e.g. ``Doorman``) while the + game data stores the display name (e.g. ``The Doorman``). This function tries an + exact match first, then falls back to stripping a leading ``The `` prefix. + """ + if raw_prefix in heroes: + return raw_prefix + for hero in heroes: + if hero.startswith('The ') and raw_prefix == hero[4:]: + return hero + return None + + +def _classify(entry: str, heroes: List[str], items: List[str], game_modes: List[str]) -> Tuple[str, Optional[str], str]: + """ + Determines whether an entry belongs to a game mode, hero, item, or is general. + + The first ``:`` in the entry separates the prefix (entity name) from the body. + Priority order: mode > hero > item > general. + """ + idx = entry.find(':') + if idx != -1: + raw_prefix = entry[:idx].strip() + rest = entry[idx + 1 :].strip() + + if raw_prefix in game_modes: + return 'mode', raw_prefix, rest + + matched_hero = _match_hero_prefix(raw_prefix, heroes) + if matched_hero: + return 'hero', matched_hero, rest + + if raw_prefix in items: + return 'item', raw_prefix, rest + + return 'general', None, entry + + +def _format_body( + text: str, + heroes: List[str], + abilities: List[str], + items: List[str], + link_targets: Optional[Dict[str, list]], + excluded_ability: Optional[str] = None, +) -> str: + # Tokenization strategy: replace matched entities with temporary null-byte tokens. + # This prevents nested replacements (e.g. matching "Melee" inside an already-generated + # "{{Heavy Melee}}" icon template). Tokens are restored at the end. + protected: List[str] = [] + + def protect(value: str) -> str: + token = f'\x00{len(protected)}\x00' + protected.append(value) + return token + + def wrap(terms: List[str], template_name: str) -> None: + nonlocal text + # Process longer names first so "Smoke Bomb" is matched before "Smoke". + for term in sorted(dict.fromkeys(terms), key=len, reverse=True): + if not term: + continue + pattern = re.compile(r'(? item > hero. + # Exclude the ability that was already used as a sub-heading to avoid redundancy. + ability_terms = [a for a in abilities if a != excluded_ability] + wrap(ability_terms, 'AbilityIcon') + wrap(items, 'ItemIcon') + wrap(heroes, 'HeroIcon') + + # Apply link_targets auto-links for terms not already claimed by icon templates. if link_targets: + claimed = set(ability_terms) | set(items) | set(heroes) for page_name, aliases in link_targets.items(): for term in aliases: - # Skip if this name is already handled by an Icon template (Hero/Ability/Item) - if term in entity_to_template: + if term in claimed or term.isdigit(): continue + pattern = re.compile(r'(? Optional[Tuple[int, int, str]]: + """ + Finds the leftmost ability name in *text*. + + Returns ``(start, end, ability_name)`` or ``None``. When multiple abilities start at + the same position the longer name wins (e.g. ``Heavy Barrage`` over ``Barrage``). + """ + best: Optional[Tuple[int, int, str]] = None + for ability in sorted(dict.fromkeys(abilities), key=len, reverse=True): + pattern = re.compile(r'(? (best[1] - best[0])): + best = candidate + return best - # Convert line-starting hyphens to asterisks for wiki lists. - wikitext = re.sub(r'^- ', '* ', raw_text, flags=re.MULTILINE) - if not sorted_names: - return wikitext +def _remove_ability_mention(text: str, start: int, end: int) -> str: + """Strips the ability name from *text* and cleans up surrounding punctuation.""" + remaining = text[:start] + text[end:] + remaining = re.sub(r'^[\s:\-\u2013\u2014]+', '', remaining) + return re.sub(r'\s+', ' ', remaining).strip() + + +def _capitalize_first(text: str) -> str: + return text[:1].upper() + text[1:] if text else text + + +def _build_output(general, modes, mode_order, item_sections, item_order, hero_sections, hero_order) -> str: + """ + Assembles the final wikitext string from the categorized entry lists. + + Output section order: + 1. General entries (no prefix match) + 2. Game mode sections (``== [[Mode]] ==``) + 3. ``== Items ==`` with per-item sub-headings + 4. ``== Heroes ==`` with per-hero and per-ability sub-headings + """ + lines: List[str] = [] + + for entry in general: + lines.append(entry) - # Create a single regex to find all entity names. The pattern is built from the - # length-sorted list, using word boundaries (\b) to prevent partial matches. - pattern_str = r'\b(' + '|'.join(re.escape(name) for name in sorted_names) + r')\b' - pattern = re.compile(pattern_str) + for mode in mode_order: + lines.append('') + lines.append(f'== [[{mode}]] ==') + for body in modes[mode]: + lines.append(body) - # Define a replacer function to look up the template for a matched name. - def replace_with_template(match: re.Match) -> str: - matched_name = match.group(0) - # Fallback to the original name if it's somehow not in the map. - return entity_to_template.get(matched_name, matched_name) + if item_order: + lines.append('') + lines.append('== Items ==') + for item in item_order: + lines.append(f'=== {{{{ItemIcon|{item}}}}} ===') + lines.extend(item_sections[item]) - # Apply the replacement across the entire text. - wikitext = pattern.sub(replace_with_template, wikitext) + if hero_order: + lines.append('') + lines.append('== Heroes ==') + for hero in hero_order: + section = hero_sections[hero] + lines.append(f'=== {{{{HeroIcon|{hero}}}}} ===') + lines.extend(section['general']) + for ability in section['ability_order']: + lines.append(f'==== {{{{AbilityIcon|{ability}}}}} ====') + lines.extend(section['ability_sections'][ability]) - return wikitext + return '\n'.join(lines).strip() diff --git a/src/deadbot.py b/src/deadbot.py index 567c39ca..28bf0c18 100644 --- a/src/deadbot.py +++ b/src/deadbot.py @@ -104,12 +104,10 @@ def act_gamefile_parse(args: Args): def act_changelog_parse(args: Args): - herolab_patch_notes_path = os.path.join(args.workdir, 'localizations', 'patch_notes', 'citadel_patch_notes_english.json') chlog_fetcher = fetch_changelogs.ChangelogFetcher( update_existing=False, input_dir=args.inputdir, output_dir=args.output, - herolab_patch_notes_path=herolab_patch_notes_path, ) chlog_fetcher.run() diff --git a/src/decompiler/decompiler.py b/src/decompiler/decompiler.py index a7e9b050..c01d968a 100644 --- a/src/decompiler/decompiler.py +++ b/src/decompiler/decompiler.py @@ -77,7 +77,6 @@ def _import_localization_data(self): 'citadel_heroes', 'citadel_main', 'citadel_mods', - 'citadel_patch_notes', ] # Loop through each folder in the array diff --git a/src/wiki/upload.py b/src/wiki/upload.py index 4963c528..5c2bcb50 100644 --- a/src/wiki/upload.py +++ b/src/wiki/upload.py @@ -117,6 +117,7 @@ def _upload_changelog_pages(self): for filename in files: changelog_id = filename.replace('.txt', '') + try: date_obj = changelog_utils.parse_changelog_date_from_id(changelog_id) if not date_obj: