From 47f482ef4e7bba5ddfa019dfca4f5976bb14d0a2 Mon Sep 17 00:00:00 2001 From: LVL1024 <70866179+LVL1024@users.noreply.github.com> Date: Fri, 3 Jul 2026 17:00:14 +0000 Subject: [PATCH 01/14] feat: migrate changelog fetching from forum RSS to Steam API --- pyproject.toml | 2 - src/changelogs/constants.py | 6 +- src/changelogs/fetch_changelogs.py | 483 ++++++++++++----------------- src/changelogs/parse_changelogs.py | 18 +- src/wiki/upload.py | 9 + 5 files changed, 226 insertions(+), 292 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 626c6a7c..6bb93753 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,9 +14,7 @@ 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" 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..07782679 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,26 @@ 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] + was_edited: NotRequired[bool] 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,7 +52,7 @@ 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): @@ -62,11 +64,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.STEAM_NEWS_URL = STEAM_NEWS_API_URL + self.APP_ID = STEAM_APP_ID self.HEROLABS_PATCH_NOTES_PATH = herolab_patch_notes_path @@ -153,23 +152,49 @@ def _get_patch_section(self, header: str, full_text: str) -> str: match = pattern.search(full_text) return match.group(0).strip() if match else '' + 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) + text = re.sub(r'\[steamitem[^\]]*\].*?\[/steamitem\]', '', text) + text = re.sub(r'\[previewyoutube[^\]]*\].*?\[/previewyoutube\]', '', text) + + # Catch-all: remove any remaining BBCode tags we missed + text = re.sub(r'\[/?[a-zA-Z0-9]+(?:\s[^\]]*)?\]', '', text) + + # Clean up extra blank lines + text = re.sub(r'\n{3,}', '\n\n', text).strip() + return text + def run(self): self.load_localization() - self.fetch_forum_changelogs() + self.fetch_steam_changelogs() self.get_gamefile_changelogs() self.changelogs_to_file() def load_localization(self): self.localization_data_en = json_utils.read(os.path.join(self.OUTPUT_DIR, 'localizations', 'english.json')) - def get_txt(self, changelog_path): - self._process_local_changelogs(changelog_path) - return self.changelogs - 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,74 +222,166 @@ 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') - - # Dictionary to group posts by date string 'YYYY-MM-DD' - content_by_date = defaultdict(list) + def fetch_steam_changelogs(self): + """Fetch patch notes from Steam Web API.""" + logger.trace('Fetching Steam news for changelogs') + target_tz = ZoneInfo('US/Pacific') - # XenForo posts are wrapped in
- articles = soup.find_all('article', class_='message') + 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 - # Target Timezone: Valve HQ (US/Pacific) - target_tz = ZoneInfo('US/Pacific') + news_items = data.get('appnews', {}).get('newsitems', []) + updates_by_day: dict[str, list[ForumUpdate]] = defaultdict(list) - 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'): + for item in news_items: + tags = item.get('tags', []) + if 'patchnotes' not in tags: + logger.trace(f"Skipping non-patch-note news: {item.get('title')} (Tags: {tags})") continue - 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_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}') + 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', '') + + dt_utc = datetime.fromtimestamp(date_unix, tz=ZoneInfo('UTC')) + dt_valve = dt_utc.astimezone(target_tz) + post_date = dt_valve.strftime('%Y-%m-%d') + + # 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 - # 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 + 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() + + updates_by_day[post_date].append({'version': gid, 'text': text, 'link': url, 'title': title, 'steam_hash': steam_hash}) + + for date_key, entries in updates_by_day.items(): + changelog_id = date_key + + 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 '' + + 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 = [] + + if existing_config: + matched = False + for e in entries: + 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!') + self.changelog_configs[changelog_id] = { + 'source_id': e['version'], + 'date': date_key, + 'link': e['link'], + 'is_hero_lab': False, + 'title': e.get('title'), + 'steam_hash': e['steam_hash'], + 'was_edited': True, + } + break + if existing_config and self.changelog_configs.get(changelog_id, {}).get('was_edited'): + continue + + if not matched and entries: + main_entry = entries[0] + + for e in entries: + if e != main_entry: + append_entries.append(e) 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 + entries.sort(key=lambda x: x['version']) + main_entry = entries[0] + append_entries = entries[1:] - text = content_div.text.strip() - if text: - content_by_date[post_date].append({'text': text, 'link': post_link}) + if local_content: + current_text = local_content + elif wiki_content and main_entry: + 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: + current_text = main_entry['text'] if main_entry else (self.changelogs.get(changelog_id, '')) + + old_text = local_content or wiki_content or '' + + def get_next_patch_num(txt): + matches = re.findall(r'=== Patch (\d+) ===', txt) + if matches: + return max(map(int, matches)) + 1 + return 2 if txt.strip() else 1 + + final_text = current_text + for entry in append_entries: + entry_text = entry['text'] + entry_text = re.sub(r'=== Patch \d+ ===\n*', '', entry_text).strip() + + 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}' + logger.debug(f'Adding Patch {patch_num} to {date_key}') + + 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) + if section_text: + self.hotfixes.append({'date': date_key, 'text': section_text}) + logger.debug(f'Detected hotfix: {h} for {date_key}') + + self.changelogs[changelog_id] = final_text + + if main_entry: + source_id = main_entry['version'] + link = main_entry['link'] + title = main_entry.get('title') + steam_hash = main_entry.get('steam_hash') + elif existing_config: + source_id = existing_config.get('source_id') + link = existing_config.get('link') + title = existing_config.get('title') + steam_hash = existing_config.get('steam_hash') + elif append_entries: + source_id = append_entries[0]['version'] + link = append_entries[0]['link'] + title = append_entries[0].get('title') + steam_hash = append_entries[0].get('steam_hash') + else: + source_id = None + link = None + title = None + steam_hash = None - return content_by_date + self.changelog_configs[changelog_id] = { + 'source_id': source_id, + 'date': date_key, + 'link': link, + 'is_hero_lab': False, + 'title': title, + 'steam_hash': steam_hash, + } def get_gamefile_changelogs(self): """Read and parse the Hero Labs changelogs in the game files""" @@ -330,7 +447,7 @@ def get_gamefile_changelogs(self): # 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, + 'source_id': None, 'date': date, 'link': None, 'is_hero_lab': True, @@ -380,208 +497,6 @@ def _localize(self, key): 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: - continue - - if version is None or version == '': - continue - - try: - # Scrape this thread for date-grouped posts - thread_updates = self._fetch_update_html(entry.link) - except Exception as e: - logger.error(f'Issue with parsing RSS feed item {entry.link}: {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 = [] - - # Find the main entry - if existing_config: - for e in entries: - if e['version'] == existing_config['forum_id']: - main_entry = e - break - for e in entries: - if e != main_entry: - append_entries.append(e) - else: - entries.sort(key=lambda x: x['version']) - main_entry = entries[0] - append_entries = entries[1:] - - # Determine base content and whether to append - if local_content: - # Local file exists - use it as base, don't append to file - # (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') - append_entries.insert(0, main_entry) - current_text = wiki_content - main_entry = None - else: - # No existing content - use forum 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) - if matches: - return max(map(int, matches)) + 1 - # If there's existing content but no patches yet, this will be Patch 2 - return 2 if txt.strip() else 1 - - # Merge any secondary entries into current text with proper patch headers - final_text = current_text - 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() - - # 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}' - logger.debug(f'Adding Patch {patch_num} to {date_key}') - - # Detect hotfixes - compare new patches against old content - 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) - if section_text: - self.hotfixes.append({'date': date_key, 'text': section_text}) - logger.debug(f'Detected hotfix: {h} for {date_key}') - - # 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 - - self.changelog_configs[changelog_id] = { - 'forum_id': forum_id, - 'date': date_key, - 'link': link, - 'is_hero_lab': False, - } - - 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): """ diff --git a/src/changelogs/parse_changelogs.py b/src/changelogs/parse_changelogs.py index 5478a044..203de509 100644 --- a/src/changelogs/parse_changelogs.py +++ b/src/changelogs/parse_changelogs.py @@ -130,9 +130,17 @@ def format_and_save_wikitext_changelogs(self, changelogs, changelog_configs): pass source_link = config.get('link', '') - source_title = '' - if source_link: - # Logic to extract title from URL + source_title = config.get('title') + + is_steam_link = ( + 'store.steampowered.com/news/' in source_link + or 'steamstore-a.akamaihd.net/news/' in source_link + or 'steamcommunity.com/app/' in source_link + ) + if source_link and is_steam_link: + if not source_title: + source_title = f"{date_obj.strftime('%m-%d-%Y')} Update" + elif source_link: parts = source_link.split('/') slug = '' for part in parts: @@ -145,11 +153,11 @@ def format_and_save_wikitext_changelogs(self, changelogs, changelog_configs): 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" + if not source_title: + source_title = f"{date_obj.strftime('%m-%d-%Y')} Update" full_page_content = f"""{{{{Update layout | prev_update = {prev_update_link} diff --git a/src/wiki/upload.py b/src/wiki/upload.py index 4963c528..0a1c14a5 100644 --- a/src/wiki/upload.py +++ b/src/wiki/upload.py @@ -107,6 +107,10 @@ def _upload_changelog_pages(self): logger.info('Uploading changelog pages...') + # Load changelog configs to check for edit flags + changelog_configs_path = os.path.join(self.OUTPUT_DIR, 'changelogs', 'changelog_configs.json') + changelog_configs = json_utils.read(changelog_configs_path, ignore_error=True) or {} + # Get and sort files with proper variant ordering via utility files = changelog_utils.sort_changelog_files([f for f in os.listdir(changelog_dir) if f.endswith('.txt')]) if not files: @@ -117,6 +121,11 @@ def _upload_changelog_pages(self): for filename in files: changelog_id = filename.replace('.txt', '') + + # Skip if the post was edited by devs to protect manual wiki edits + if changelog_configs.get(changelog_id, {}).get('was_edited'): + logger.info(f'Skipping wiki upload for {changelog_id} because devs edited the post. Manual review required.') + continue try: date_obj = changelog_utils.parse_changelog_date_from_id(changelog_id) if not date_obj: From 0db149fc6c7fe22fd7d88e16fc7dcc75fda29502 Mon Sep 17 00:00:00 2001 From: LVL1024 <70866179+LVL1024@users.noreply.github.com> Date: Fri, 3 Jul 2026 17:10:31 +0000 Subject: [PATCH 02/14] ci: re-enable changelogs in deploy workflow --- .github/workflows/deploy.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 }} From 15e4e51b891890700fd8da2afde7905f65f83b9e Mon Sep 17 00:00:00 2001 From: LVL1024 <70866179+LVL1024@users.noreply.github.com> Date: Fri, 3 Jul 2026 17:14:44 +0000 Subject: [PATCH 03/14] chore: update poetry.lock after removing feedparser and bs4 --- poetry.lock | 588 +++++++++++++++++++++------------------------------- 1 file changed, 231 insertions(+), 357 deletions(-) diff --git a/poetry.lock b/poetry.lock index 52ba9822..9e6840e4 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,53 +1,14 @@ -# 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 = "*" +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. [[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]] @@ -56,7 +17,6 @@ version = "3.5.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.10" -groups = ["dev"] files = [ {file = "cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0"}, {file = "cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132"}, @@ -68,7 +28,6 @@ version = "3.4.7" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d"}, {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8"}, @@ -207,8 +166,6 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main"] -markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -216,41 +173,24 @@ 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]] @@ -259,7 +199,6 @@ version = "2.6.19" description = "File identification library for Python" optional = false python-versions = ">=3.10" -groups = ["dev"] files = [ {file = "identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a"}, {file = "identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842"}, @@ -274,7 +213,6 @@ version = "3.18" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, @@ -289,7 +227,6 @@ version = "0.1" description = "Read and write Valve's KeyValues3 format" optional = false python-versions = ">=3.11" -groups = ["main"] files = [ {file = "keyvalues3-0.1-py3-none-any.whl", hash = "sha256:0a20354d3fcab856d84386bddf186e29b113a785a7253dfee705494078d20539"}, {file = "keyvalues3-0.1.tar.gz", hash = "sha256:37b894c71cfa071758895401f833d0ccacd5fdb99063a72681ab6079a674c2a1"}, @@ -308,7 +245,6 @@ version = "0.7.3" description = "Python logging made (stupidly) simple" optional = false python-versions = "<4.0,>=3.5" -groups = ["main"] files = [ {file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}, {file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}, @@ -319,7 +255,7 @@ colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} [package.extras] -dev = ["Sphinx (==8.1.3) ; python_version >= \"3.11\"", "build (==1.2.2) ; python_version >= \"3.11\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.5.0) ; python_version >= \"3.8\"", "mypy (==v0.910) ; python_version < \"3.6\"", "mypy (==v0.971) ; python_version == \"3.6\"", "mypy (==v1.13.0) ; python_version >= \"3.8\"", "mypy (==v1.4.1) ; python_version == \"3.7\"", "myst-parser (==4.0.0) ; python_version >= \"3.11\"", "pre-commit (==4.0.1) ; python_version >= \"3.9\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==8.3.2) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==5.0.0) ; python_version == \"3.8\"", "pytest-cov (==6.0.0) ; python_version >= \"3.9\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.1.0) ; python_version >= \"3.8\"", "sphinx-rtd-theme (==3.0.2) ; python_version >= \"3.11\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.23.2) ; python_version >= \"3.8\"", "twine (==6.0.1) ; python_version >= \"3.11\""] +dev = ["Sphinx (==8.1.3)", "build (==1.2.2)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.5.0)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.13.0)", "mypy (==v1.4.1)", "myst-parser (==4.0.0)", "pre-commit (==4.0.1)", "pytest (==6.1.2)", "pytest (==8.3.2)", "pytest-cov (==2.12.1)", "pytest-cov (==5.0.0)", "pytest-cov (==6.0.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.1.0)", "sphinx-rtd-theme (==3.0.2)", "tox (==3.27.1)", "tox (==4.23.2)", "twine (==6.0.1)"] [[package]] name = "lz4" @@ -327,7 +263,6 @@ version = "4.4.5" description = "LZ4 Bindings for Python" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "lz4-4.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d221fa421b389ab2345640a508db57da36947a437dfe31aeddb8d5c7b646c22d"}, {file = "lz4-4.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7dc1e1e2dbd872f8fae529acd5e4839efd0b141eaa8ae7ce835a9fe80fbad89f"}, @@ -399,7 +334,6 @@ version = "0.11.0" description = "MediaWiki API client" optional = false python-versions = "*" -groups = ["main"] files = [ {file = "mwclient-0.11.0-py3-none-any.whl", hash = "sha256:27914a307cb4539fd49a16d634c9c777c0de0976b3af9225bd95d4657bfd4d74"}, {file = "mwclient-0.11.0.tar.gz", hash = "sha256:2bb7696f3703243eb33514ab162d7f6076dcedd011be90682936046c5e34fd06"}, @@ -414,7 +348,6 @@ version = "1.10.0" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] files = [ {file = "nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827"}, {file = "nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"}, @@ -422,13 +355,12 @@ 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] @@ -444,7 +376,6 @@ version = "3.3.1" description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" optional = false python-versions = ">=3.8" -groups = ["main"] files = [ {file = "oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1"}, {file = "oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9"}, @@ -461,7 +392,6 @@ version = "0.11.0" description = "(Soon to be) the fastest pure-Python PEG parser I could muster" optional = false python-versions = "*" -groups = ["main"] files = [ {file = "parsimonious-0.11.0-py3-none-any.whl", hash = "sha256:32e3818abf9f05b3b9f3b6d87d128645e30177e91f614d2277d88a0aea98fae2"}, {file = "parsimonious-0.11.0.tar.gz", hash = "sha256:e080377d98957beec053580d38ae54fcdf7c470fb78670ba4bf8b5f9d5cad2a9"}, @@ -475,103 +405,98 @@ 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 +504,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]] @@ -588,7 +513,6 @@ version = "4.10.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.10" -groups = ["dev"] files = [ {file = "platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a"}, {file = "platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7"}, @@ -600,7 +524,6 @@ version = "3.8.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" -groups = ["dev"] files = [ {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, @@ -615,14 +538,13 @@ 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] @@ -639,7 +561,6 @@ version = "1.2.2" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.10" -groups = ["main"] files = [ {file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"}, {file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"}, @@ -654,7 +575,6 @@ version = "0.1.3" description = "A (quite) simple that helps creating on-the-fly Mermaid diagrams" optional = false python-versions = ">=3.10,<4.0" -groups = ["main"] files = [ {file = "python_mermaid-0.1.3-py3-none-any.whl", hash = "sha256:7d283d98b21b97feb428593905fb8b35e4cd8dbd038aa3ef1e09118d4a91ea89"}, {file = "python_mermaid-0.1.3.tar.gz", hash = "sha256:8c9cd781fdfc8ed958513782a982254037913f3d1d893018691044bafa3bd441"}, @@ -669,7 +589,6 @@ version = "6.0.3" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" -groups = ["dev"] files = [ {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, @@ -748,126 +667,125 @@ 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]] @@ -876,7 +794,6 @@ version = "2.34.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.10" -groups = ["main"] files = [ {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, @@ -898,7 +815,6 @@ version = "2.0.0" description = "OAuthlib authentication support for Requests." optional = false python-versions = ">=3.4" -groups = ["main"] files = [ {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, @@ -917,7 +833,6 @@ version = "0.6.9" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" -groups = ["dev"] files = [ {file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"}, {file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"}, @@ -939,48 +854,12 @@ files = [ {file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"}, ] -[[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." -optional = false -python-versions = ">=3.9" -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"}, -] - [[package]] name = "unidecode" version = "1.4.0" description = "ASCII transliterations of Unicode text" optional = false python-versions = ">=3.7" -groups = ["main"] files = [ {file = "Unidecode-1.4.0-py3-none-any.whl", hash = "sha256:c3c7606c27503ad8d501270406e345ddb480a7b5f38827eafe4fa82a137f0021"}, {file = "Unidecode-1.4.0.tar.gz", hash = "sha256:ce35985008338b676573023acc382d62c264f307c8f7963733405add37ea2b23"}, @@ -992,35 +871,33 @@ version = "2.7.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.10" -groups = ["main"] files = [ {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, ] [package.extras] -brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] +brotli = ["brotli (>=1.2.0)", "brotlicffi (>=1.2.0.0)"] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] +zstd = ["backports-zstd (>=1.0.0)"] [[package]] name = "virtualenv" -version = "21.4.2" +version = "21.5.1" description = "Virtual Python Environment builder" optional = false -python-versions = ">=3.8" -groups = ["dev"] +python-versions = ">=3.9" 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" @@ -1028,15 +905,13 @@ version = "1.2.0" description = "A small Python utility to set file creation time on Windows" optional = false python-versions = ">=3.5" -groups = ["main"] -markers = "sys_platform == \"win32\"" files = [ {file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"}, {file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"}, ] [package.extras] -dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] [[package]] name = "zstandard" @@ -1044,7 +919,6 @@ version = "0.25.0" description = "Zstandard bindings for Python" optional = false python-versions = ">=3.9" -groups = ["main"] files = [ {file = "zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd"}, {file = "zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7"}, @@ -1148,9 +1022,9 @@ files = [ ] [package.extras] -cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and python_version < \"3.14\"", "cffi (>=2.0.0b) ; platform_python_implementation != \"PyPy\" and python_version >= \"3.14\""] +cffi = ["cffi (>=1.17,<2.0)", "cffi (>=2.0.0b)"] [metadata] -lock-version = "2.1" +lock-version = "2.0" python-versions = ">=3.11,<3.15" -content-hash = "9fa7378a8df2620a9ee54a7b989622a809fabff10605969a57e54021d73d671c" +content-hash = "7c17f18c77d55785b984084f19bb954bb4c5d3a54462afc5818fcdf20b37b1b7" From 54e3a09b01e4da71e88c64363e7f57a2f6f92c7e Mon Sep 17 00:00:00 2001 From: LVL1024 <70866179+LVL1024@users.noreply.github.com> Date: Fri, 3 Jul 2026 17:30:18 +0000 Subject: [PATCH 04/14] fix: add tzdata dependency for ZoneInfo in Docker --- poetry.lock | 13 ++++++++++++- pyproject.toml | 1 + 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/poetry.lock b/poetry.lock index 9e6840e4..9cce83f3 100644 --- a/poetry.lock +++ b/poetry.lock @@ -854,6 +854,17 @@ files = [ {file = "ruff-0.6.9.tar.gz", hash = "sha256:b076ef717a8e5bc819514ee1d602bbdca5b4420ae13a9cf61a0c0a4f53a2baa2"}, ] +[[package]] +name = "tzdata" +version = "2024.2" +description = "Provider of IANA time zone data" +optional = false +python-versions = ">=2" +files = [ + {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, + {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, +] + [[package]] name = "unidecode" version = "1.4.0" @@ -1027,4 +1038,4 @@ cffi = ["cffi (>=1.17,<2.0)", "cffi (>=2.0.0b)"] [metadata] lock-version = "2.0" python-versions = ">=3.11,<3.15" -content-hash = "7c17f18c77d55785b984084f19bb954bb4c5d3a54462afc5818fcdf20b37b1b7" +content-hash = "98215fe1e749b03338f96fc7994319c71b530765c4d0711672f2e83c25f163db" diff --git a/pyproject.toml b/pyproject.toml index 6bb93753..4db01ef2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ keyvalues3="^0.1" python-mermaid="^0.1.3" python-dotenv = "^1.0.1" requests = "^2.32.3" +tzdata = "^2024.2" loguru = "^0.7.2" zstandard = "^0.25.0" pillow = "^12.2.0" From 3d12d17c07d79b0ef655db7b6dcccd45ec67fa56 Mon Sep 17 00:00:00 2001 From: Deadbot0 Date: Fri, 3 Jul 2026 17:34:40 +0000 Subject: [PATCH 05/14] [skip ci] chore: updated input data --- input-data/changelogs/changelog_configs.json | 32 ++++ input-data/changelogs/raw/2026-06-04.txt | 18 ++ input-data/changelogs/raw/2026-06-11.txt | 105 +++++++++++ input-data/changelogs/raw/2026-06-30.txt | 187 +++++++++++++++++++ input-data/changelogs/raw/2026-07-01.txt | 5 + 5 files changed, 347 insertions(+) create mode 100644 input-data/changelogs/raw/2026-06-04.txt create mode 100644 input-data/changelogs/raw/2026-06-11.txt create mode 100644 input-data/changelogs/raw/2026-06-30.txt create mode 100644 input-data/changelogs/raw/2026-07-01.txt diff --git a/input-data/changelogs/changelog_configs.json b/input-data/changelogs/changelog_configs.json index d433e5de..177eccd5 100644 --- a/input-data/changelogs/changelog_configs.json +++ b/input-data/changelogs/changelog_configs.json @@ -760,5 +760,37 @@ "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" } } \ 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 From 47c184021d399ed08c254748e7f69e758cb4f16f Mon Sep 17 00:00:00 2001 From: LVL1024 <70866179+LVL1024@users.noreply.github.com> Date: Fri, 3 Jul 2026 18:52:29 +0000 Subject: [PATCH 06/14] chore: remove Hero Lab changelog parsing (no longer used by devs) --- src/changelogs/fetch_changelogs.py | 142 +---------------------------- src/changelogs/parse_changelogs.py | 12 +-- src/changelogs/tags.py | 1 - src/deadbot.py | 2 - 4 files changed, 2 insertions(+), 155 deletions(-) diff --git a/src/changelogs/fetch_changelogs.py b/src/changelogs/fetch_changelogs.py index 07782679..942c847b 100644 --- a/src/changelogs/fetch_changelogs.py +++ b/src/changelogs/fetch_changelogs.py @@ -55,7 +55,7 @@ class ChangelogFetcher: 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] = [] @@ -67,8 +67,6 @@ def __init__(self, update_existing, input_dir, output_dir, herolab_patch_notes_p self.STEAM_NEWS_URL = STEAM_NEWS_API_URL self.APP_ID = STEAM_APP_ID - self.HEROLABS_PATCH_NOTES_PATH = herolab_patch_notes_path - self.TAGS_TO_REMOVE = ['
    ', '
', '', '', '', ''] self.wiki_site = None @@ -185,7 +183,6 @@ def _bbcode_to_text(self, bbcode: str) -> str: def run(self): self.load_localization() self.fetch_steam_changelogs() - self.get_gamefile_changelogs() self.changelogs_to_file() def load_localization(self): @@ -382,140 +379,3 @@ def get_next_patch_num(txt): 'title': title, 'steam_hash': steam_hash, } - - 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] = { - 'source_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 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/parse_changelogs.py b/src/changelogs/parse_changelogs.py index 203de509..8023b9af 100644 --- a/src/changelogs/parse_changelogs.py +++ b/src/changelogs/parse_changelogs.py @@ -93,15 +93,12 @@ def format_and_save_wikitext_changelogs(self, changelogs, changelog_configs): output_path = os.path.join(self.OUTPUT_DIR, 'changelogs', 'wiki') os.makedirs(output_path, exist_ok=True) - # Sort keys to determine chronological order - # We filter for valid dates and exclude 'is_hero_lab' so main updates only link to previous main updates. sorted_update_ids = sorted( [k for k, v in changelog_configs.items() if v.get('date') and not v.get('is_hero_lab')], key=lambda k: changelog_configs[k]['date'] ) 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. if not config or config.get('is_hero_lab'): continue @@ -129,7 +126,7 @@ def format_and_save_wikitext_changelogs(self, changelogs, changelog_configs): except ValueError: pass - source_link = config.get('link', '') + source_link = config.get('link') or '' source_title = config.get('title') is_steam_link = ( @@ -236,13 +233,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) 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/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() From b7dd29c7ee59817516d2ba743e7cf73c4d74705f Mon Sep 17 00:00:00 2001 From: LVL1024 <70866179+LVL1024@users.noreply.github.com> Date: Fri, 3 Jul 2026 19:30:49 +0000 Subject: [PATCH 07/14] feat: restructure changelog formatting with section grouping, ability subheaders, and hotfix preservation --- src/changelogs/parse_changelogs.py | 8 + src/changelogs/wikitext_formatter.py | 317 +++++++++++++++++++++------ 2 files changed, 260 insertions(+), 65 deletions(-) diff --git a/src/changelogs/parse_changelogs.py b/src/changelogs/parse_changelogs.py index 8023b9af..a225652b 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 . import constants class ChangelogParser: @@ -107,6 +108,13 @@ 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 + # Only process changelogs from the Steam API era (on or after + # STEAM_MIGRATION_DATE) to avoid reformatting old changelogs that + # are already stable on the wiki with the previous formatter output. + if changelog_date < constants.STEAM_MIGRATION_DATE: + logger.trace(f"Skipping '{changelog_id}' — pre-migration changelog.") + continue + formatted_body = wikitext_formatter.format_changelog(raw_text, hero_data, item_data, ability_data, link_targets=link_targets) date_obj = datetime.strptime(changelog_date, '%Y-%m-%d') diff --git a/src/changelogs/wikitext_formatter.py b/src/changelogs/wikitext_formatter.py index d13f08b7..f1da9411 100644 --- a/src/changelogs/wikitext_formatter.py +++ b/src/changelogs/wikitext_formatter.py @@ -1,5 +1,29 @@ import re -from typing import Dict, Any, Optional +from typing import Dict, Any, Optional, List, Tuple + +GAME_MODE_SECTIONS = ['Street Brawl'] + +GENERIC_HEADERS = { + 'general', + 'general changes', + 'items', + 'items changes', + 'item gameplay', + 'item gameplay changes', + 'weapon items', + 'vitality items', + 'spirit items', + 'new items', + 'heroes', + 'hero changes', + 'hero gameplay', + 'hero gameplay changes', + 'misc gameplay', + 'misc gameplay changes', + 'gameplay changes', + 'abilities', + 'ability changes', +} def format_changelog( @@ -9,85 +33,248 @@ def format_changelog( ability_data: Dict[str, Any], 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. - - 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. - - Returns: - str: The formatted wikitext string. - """ if not raw_text: return '' - # Create a mapping from entity names to their wikitext templates. - entity_to_template = {} + heroes = [v.get('Name') for v in hero_data.values() if v.get('Name') and not v.get('IsDisabled')] + items = [v.get('Name') for v in item_data.values() if v.get('Name') and not v.get('IsDisabled')] + abilities = [v.get('Name') for v in ability_data.values() if v.get('Name') and not v.get('IsDisabled')] + + parts = re.split(r'(=== Patch \d+ ===)', raw_text) + + patch_blocks = [] + if parts[0].strip(): + patch_blocks.append(('Patch 1', parts[0])) + + for i in range(1, len(parts), 2): + if i + 1 < len(parts): + header = parts[i].strip('= ').strip() + patch_blocks.append((header, parts[i + 1])) + + out = [] + is_first_patch = True + + for patch_name, patch_text in patch_blocks: + if not is_first_patch or patch_name != 'Patch 1': + out.append(f'=== {patch_name} ===') + out.append('') + is_first_patch = False + + entries = _parse_entries(patch_text) + out.extend(_build_output(entries, heroes, items, abilities, link_targets)) + out.append('') + + return '\n'.join(out).strip() + '\n' + + +def _parse_entries(raw_text: str) -> List[Tuple[str, str]]: + entries = [] + current = None + + for line in raw_text.split('\n'): + stripped = line.strip() + if not stripped: + if current is not None: + entries.append(('entry', current)) + current = None + continue - # 1. High Priority: Game Entities (Heroes, Abilities, Items) get Icons - data_sources = [ - (hero_data, 'HeroIcon'), - (ability_data, 'AbilityIcon'), - (item_data, 'ItemIcon'), - ] + if stripped.startswith('[') and stripped.endswith(']'): + if current is not None: + entries.append(('entry', current)) + current = None - for data, template_name in data_sources: - if not data: + header_text = stripped[1:-1].strip() + if header_text.lower() not in GENERIC_HEADERS: + entries.append(('header', header_text)) 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. + + if stripped.startswith('-') or stripped.startswith('*'): + if current is not None: + entries.append(('entry', current)) + current = stripped.lstrip('-* ').strip() + elif current is not None: + current += ' ' + stripped + + if current is not None: + entries.append(('entry', current)) + + cleaned_entries = [] + for kind, text in entries: + if kind == 'entry': + cleaned_entries.append((kind, re.sub(r'\s+', ' ', text).strip())) + else: + cleaned_entries.append((kind, text)) + return cleaned_entries + + +def _classify_entry(entry: str, heroes: list, items: list, game_modes: list) -> Tuple[str, Optional[str], str]: + idx = entry.find(':') + if idx != -1: + prefix = entry[:idx].strip() + rest = entry[idx + 1 :].strip() + + if prefix in game_modes: + return ('mode', prefix, rest) + if prefix in heroes: + return ('hero', prefix, rest) + if prefix in items: + return ('item', prefix, rest) + + return ('general', None, entry) + + +def _find_and_remove_ability(text: str, abilities: list) -> Tuple[Optional[str], str]: + best_match = {'ability': None, 'index': float('inf'), 'length': 0} + + for ability in sorted(abilities, key=len, reverse=True): + pattern = re.compile(r'(? best_match['length']): + best_match = {'ability': ability, 'index': match.start(), 'length': len(match.group(0))} + + if best_match['ability']: + start, end = best_match['index'], best_match['index'] + best_match['length'] + cleaned_text = text[:start] + text[end:] + cleaned_text = re.sub(r'^[\s:–—-]+', '', cleaned_text).strip() + cleaned_text = re.sub(r'\s+', ' ', cleaned_text).strip() + return best_match['ability'], cleaned_text + + return None, text + + +def _format_body( + text: str, + heroes: list, + items: list, + abilities: list, + excluded_ability: Optional[str] = None, + link_targets: Optional[dict] = None, +) -> str: + protected_tokens = [] + + def protect(value: str) -> str: + token = f'\x00{len(protected_tokens)}\x00' + protected_tokens.append(value) + return token + + text = re.sub(r'\{\{(?:Ability|Hero|Item)Icon\|[^{}]+\}\}', lambda m: protect(m.group(0)), text) + + replacements = {} + + abilities_to_wrap = [a for a in abilities if a != excluded_ability] + for name in abilities_to_wrap: + replacements[name] = f'{{{{AbilityIcon|{name}}}}}' + for name in items: + replacements[name] = f'{{{{ItemIcon|{name}}}}}' + for name in heroes: + replacements[name] = f'{{{{HeroIcon|{name}}}}}' + if link_targets: 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: + for alias in aliases: + if alias.isdigit() or alias in replacements: continue + if alias != page_name: + replacements[alias] = f'[[{page_name}|{alias}]]' + else: + replacements[alias] = f'[[{page_name}]]' - # Skip numeric pages to avoid matching parts of numbers - if term.isdigit(): - continue + sorted_terms = sorted(replacements.keys(), key=len, reverse=True) - # If term is different from page_name, use pipe to preserve text: [[Page|term]] - # Otherwise link directly: [[Page]] - if term != page_name: - entity_to_template[term] = f'[[{page_name}|{term}]]' - else: - entity_to_template[term] = f'[[{page_name}]]' + for term in sorted_terms: + pattern = re.compile(r'(? List[str]: + general = [] + modes = {} + mode_order = [] + item_sections = {} + item_order = [] + hero_sections = {} + hero_order = [] + + for kind, entry in entries: + if kind == 'header': + general.append(f'== {entry} ==') + continue + + kind, name, body = _classify_entry(entry, heroes, items, GAME_MODE_SECTIONS) + + if kind == 'general': + general.append('* ' + _format_body(body, heroes, items, abilities, link_targets=link_targets)) + + elif kind == 'mode': + if name not in modes: + modes[name] = [] + mode_order.append(name) + modes[name].append('* ' + _format_body(body, heroes, items, abilities, link_targets=link_targets)) + + elif kind == 'item': + if name not in item_sections: + item_sections[name] = [] + item_order.append(name) + item_sections[name].append('* {{Change|}} ' + _format_body(body, heroes, items, abilities, link_targets=link_targets)) + + elif kind == 'hero': + if name not in hero_sections: + hero_sections[name] = {'general': [], 'abilities': {}, 'ability_order': []} + hero_order.append(name) + + section = hero_sections[name] + ability_name, cleaned_body = _find_and_remove_ability(body, abilities) + + if ability_name and cleaned_body: + if ability_name not in section['abilities']: + section['abilities'][ability_name] = [] + section['ability_order'].append(ability_name) + cleaned_body = cleaned_body[0].upper() + cleaned_body[1:] if cleaned_body else cleaned_body + section['abilities'][ability_name].append( + '* {{Change|}} ' + _format_body(cleaned_body, heroes, items, abilities, excluded_ability=ability_name, link_targets=link_targets) + ) + else: + section['general'].append('* {{Change|}} ' + _format_body(body, heroes, items, abilities, link_targets=link_targets)) + + out = [] - # Sort all unique entity names by length in descending order. - # This is crucial for the regex to match longer names first (e.g., "Smoke Bomb" before "Smoke"). - sorted_names = sorted(list(entity_to_template.keys()), key=len, reverse=True) + for entry in general: + out.append(entry) - # Convert line-starting hyphens to asterisks for wiki lists. - wikitext = re.sub(r'^- ', '* ', raw_text, flags=re.MULTILINE) + for mode in mode_order: + out.append('') + out.append(f'== [[{mode}]] ==') + for body in modes[mode]: + out.append(body) - if not sorted_names: - return wikitext + if item_order: + out.append('') + out.append('= Items =') + for item in item_order: + out.append(f'== {{{{ItemIcon|{item}}}}} ==') + for body in item_sections[item]: + out.append(body) - # 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) + if hero_order: + out.append('') + out.append('= Heroes =') + for hero in hero_order: + out.append(f'== {{{{HeroIcon|{hero}}}}} ==') + section = hero_sections[hero] - # 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) + for body in section['general']: + out.append(body) - # Apply the replacement across the entire text. - wikitext = pattern.sub(replace_with_template, wikitext) + for ability in section['ability_order']: + out.append(f'==== {{{{AbilityIcon|{ability}}}}} ====') + for body in section['abilities'][ability]: + out.append(body) - return wikitext + return out From 9f161420cf9a5673c8e51afa0a0a5dff308ccc67 Mon Sep 17 00:00:00 2001 From: LVL1024 <70866179+LVL1024@users.noreply.github.com> Date: Fri, 3 Jul 2026 19:31:33 +0000 Subject: [PATCH 08/14] TEST: add dry-run input to workflow_dispatch for wiki testing --- .github/workflows/deploy.yaml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index e85d252c..bbece48c 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -17,6 +17,10 @@ on: type: boolean description: Only parse English localizations default: false + dry-run: + type: boolean + description: Run in dry-run mode — logs wiki actions without actually saving + default: false workflow_call: inputs: @@ -157,7 +161,8 @@ jobs: PARSE: true CHANGELOGS: true CLEANUP: false - WIKI_UPLOAD: ${{ needs.get-branch.outputs.branch == 'master' }} + WIKI_UPLOAD: ${{ needs.get-branch.outputs.branch == 'master' || github.event.inputs.dry-run == 'true' }} + DRY_RUN: ${{ github.event.inputs.dry-run == 'true' }} STEAM_USERNAME: ${{ secrets.STEAM_USERNAME }} STEAM_PASSWORD: ${{ secrets.STEAM_PASSWORD }} BOT_WIKI_USER: ${{ vars.BOT_WIKI_USER }} From 114866e3b632b36361dc254087e77c0e6b5914b3 Mon Sep 17 00:00:00 2001 From: LVL1024 <70866179+LVL1024@users.noreply.github.com> Date: Fri, 3 Jul 2026 20:50:15 +0000 Subject: [PATCH 09/14] Revert "feat: restructure changelog formatting with section grouping, ability subheaders, and hotfix preservation" This reverts commit b7dd29c7ee59817516d2ba743e7cf73c4d74705f. --- src/changelogs/parse_changelogs.py | 8 - src/changelogs/wikitext_formatter.py | 317 ++++++--------------------- 2 files changed, 65 insertions(+), 260 deletions(-) diff --git a/src/changelogs/parse_changelogs.py b/src/changelogs/parse_changelogs.py index a225652b..8023b9af 100644 --- a/src/changelogs/parse_changelogs.py +++ b/src/changelogs/parse_changelogs.py @@ -6,7 +6,6 @@ from utils import file_utils from .tags import ChangelogTags as Tags from . import wikitext_formatter -from . import constants class ChangelogParser: @@ -108,13 +107,6 @@ 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 - # Only process changelogs from the Steam API era (on or after - # STEAM_MIGRATION_DATE) to avoid reformatting old changelogs that - # are already stable on the wiki with the previous formatter output. - if changelog_date < constants.STEAM_MIGRATION_DATE: - logger.trace(f"Skipping '{changelog_id}' — pre-migration changelog.") - continue - formatted_body = wikitext_formatter.format_changelog(raw_text, hero_data, item_data, ability_data, link_targets=link_targets) date_obj = datetime.strptime(changelog_date, '%Y-%m-%d') diff --git a/src/changelogs/wikitext_formatter.py b/src/changelogs/wikitext_formatter.py index f1da9411..d13f08b7 100644 --- a/src/changelogs/wikitext_formatter.py +++ b/src/changelogs/wikitext_formatter.py @@ -1,29 +1,5 @@ import re -from typing import Dict, Any, Optional, List, Tuple - -GAME_MODE_SECTIONS = ['Street Brawl'] - -GENERIC_HEADERS = { - 'general', - 'general changes', - 'items', - 'items changes', - 'item gameplay', - 'item gameplay changes', - 'weapon items', - 'vitality items', - 'spirit items', - 'new items', - 'heroes', - 'hero changes', - 'hero gameplay', - 'hero gameplay changes', - 'misc gameplay', - 'misc gameplay changes', - 'gameplay changes', - 'abilities', - 'ability changes', -} +from typing import Dict, Any, Optional def format_changelog( @@ -33,248 +9,85 @@ def format_changelog( ability_data: Dict[str, Any], 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. + + 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. + + Returns: + str: The formatted wikitext string. + """ if not raw_text: return '' - heroes = [v.get('Name') for v in hero_data.values() if v.get('Name') and not v.get('IsDisabled')] - items = [v.get('Name') for v in item_data.values() if v.get('Name') and not v.get('IsDisabled')] - abilities = [v.get('Name') for v in ability_data.values() if v.get('Name') and not v.get('IsDisabled')] - - parts = re.split(r'(=== Patch \d+ ===)', raw_text) - - patch_blocks = [] - if parts[0].strip(): - patch_blocks.append(('Patch 1', parts[0])) - - for i in range(1, len(parts), 2): - if i + 1 < len(parts): - header = parts[i].strip('= ').strip() - patch_blocks.append((header, parts[i + 1])) - - out = [] - is_first_patch = True - - for patch_name, patch_text in patch_blocks: - if not is_first_patch or patch_name != 'Patch 1': - out.append(f'=== {patch_name} ===') - out.append('') - is_first_patch = False - - entries = _parse_entries(patch_text) - out.extend(_build_output(entries, heroes, items, abilities, link_targets)) - out.append('') - - return '\n'.join(out).strip() + '\n' - - -def _parse_entries(raw_text: str) -> List[Tuple[str, str]]: - entries = [] - current = None - - for line in raw_text.split('\n'): - stripped = line.strip() - if not stripped: - if current is not None: - entries.append(('entry', current)) - current = None - continue + # Create a mapping from entity names to their wikitext templates. + entity_to_template = {} - if stripped.startswith('[') and stripped.endswith(']'): - if current is not None: - entries.append(('entry', current)) - current = None + # 1. High Priority: Game Entities (Heroes, Abilities, Items) get Icons + data_sources = [ + (hero_data, 'HeroIcon'), + (ability_data, 'AbilityIcon'), + (item_data, 'ItemIcon'), + ] - header_text = stripped[1:-1].strip() - if header_text.lower() not in GENERIC_HEADERS: - entries.append(('header', header_text)) + for data, template_name in data_sources: + if not data: continue - - if stripped.startswith('-') or stripped.startswith('*'): - if current is not None: - entries.append(('entry', current)) - current = stripped.lstrip('-* ').strip() - elif current is not None: - current += ' ' + stripped - - if current is not None: - entries.append(('entry', current)) - - cleaned_entries = [] - for kind, text in entries: - if kind == 'entry': - cleaned_entries.append((kind, re.sub(r'\s+', ' ', text).strip())) - else: - cleaned_entries.append((kind, text)) - return cleaned_entries - - -def _classify_entry(entry: str, heroes: list, items: list, game_modes: list) -> Tuple[str, Optional[str], str]: - idx = entry.find(':') - if idx != -1: - prefix = entry[:idx].strip() - rest = entry[idx + 1 :].strip() - - if prefix in game_modes: - return ('mode', prefix, rest) - if prefix in heroes: - return ('hero', prefix, rest) - if prefix in items: - return ('item', prefix, rest) - - return ('general', None, entry) - - -def _find_and_remove_ability(text: str, abilities: list) -> Tuple[Optional[str], str]: - best_match = {'ability': None, 'index': float('inf'), 'length': 0} - - for ability in sorted(abilities, key=len, reverse=True): - pattern = re.compile(r'(? best_match['length']): - best_match = {'ability': ability, 'index': match.start(), 'length': len(match.group(0))} - - if best_match['ability']: - start, end = best_match['index'], best_match['index'] + best_match['length'] - cleaned_text = text[:start] + text[end:] - cleaned_text = re.sub(r'^[\s:–—-]+', '', cleaned_text).strip() - cleaned_text = re.sub(r'\s+', ' ', cleaned_text).strip() - return best_match['ability'], cleaned_text - - return None, text - - -def _format_body( - text: str, - heroes: list, - items: list, - abilities: list, - excluded_ability: Optional[str] = None, - link_targets: Optional[dict] = None, -) -> str: - protected_tokens = [] - - def protect(value: str) -> str: - token = f'\x00{len(protected_tokens)}\x00' - protected_tokens.append(value) - return token - - text = re.sub(r'\{\{(?:Ability|Hero|Item)Icon\|[^{}]+\}\}', lambda m: protect(m.group(0)), text) - - replacements = {} - - abilities_to_wrap = [a for a in abilities if a != excluded_ability] - for name in abilities_to_wrap: - replacements[name] = f'{{{{AbilityIcon|{name}}}}}' - for name in items: - replacements[name] = f'{{{{ItemIcon|{name}}}}}' - for name in heroes: - replacements[name] = f'{{{{HeroIcon|{name}}}}}' - + 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. if link_targets: for page_name, aliases in link_targets.items(): - for alias in aliases: - if alias.isdigit() or alias in replacements: + for term in aliases: + # Skip if this name is already handled by an Icon template (Hero/Ability/Item) + if term in entity_to_template: continue - if alias != page_name: - replacements[alias] = f'[[{page_name}|{alias}]]' - else: - replacements[alias] = f'[[{page_name}]]' - - sorted_terms = sorted(replacements.keys(), key=len, reverse=True) - - for term in sorted_terms: - pattern = re.compile(r'(? List[str]: - general = [] - modes = {} - mode_order = [] - item_sections = {} - item_order = [] - hero_sections = {} - hero_order = [] - - for kind, entry in entries: - if kind == 'header': - general.append(f'== {entry} ==') - continue - - kind, name, body = _classify_entry(entry, heroes, items, GAME_MODE_SECTIONS) - - if kind == 'general': - general.append('* ' + _format_body(body, heroes, items, abilities, link_targets=link_targets)) - - elif kind == 'mode': - if name not in modes: - modes[name] = [] - mode_order.append(name) - modes[name].append('* ' + _format_body(body, heroes, items, abilities, link_targets=link_targets)) - - elif kind == 'item': - if name not in item_sections: - item_sections[name] = [] - item_order.append(name) - item_sections[name].append('* {{Change|}} ' + _format_body(body, heroes, items, abilities, link_targets=link_targets)) - - elif kind == 'hero': - if name not in hero_sections: - hero_sections[name] = {'general': [], 'abilities': {}, 'ability_order': []} - hero_order.append(name) - - section = hero_sections[name] - ability_name, cleaned_body = _find_and_remove_ability(body, abilities) - - if ability_name and cleaned_body: - if ability_name not in section['abilities']: - section['abilities'][ability_name] = [] - section['ability_order'].append(ability_name) - cleaned_body = cleaned_body[0].upper() + cleaned_body[1:] if cleaned_body else cleaned_body - section['abilities'][ability_name].append( - '* {{Change|}} ' + _format_body(cleaned_body, heroes, items, abilities, excluded_ability=ability_name, link_targets=link_targets) - ) - else: - section['general'].append('* {{Change|}} ' + _format_body(body, heroes, items, abilities, link_targets=link_targets)) - - out = [] + # If term is different from page_name, use pipe to preserve text: [[Page|term]] + # Otherwise link directly: [[Page]] + if term != page_name: + entity_to_template[term] = f'[[{page_name}|{term}]]' + else: + entity_to_template[term] = f'[[{page_name}]]' - for entry in general: - out.append(entry) + # Sort all unique entity names by length in descending order. + # This is crucial for the regex to match longer names first (e.g., "Smoke Bomb" before "Smoke"). + sorted_names = sorted(list(entity_to_template.keys()), key=len, reverse=True) - for mode in mode_order: - out.append('') - out.append(f'== [[{mode}]] ==') - for body in modes[mode]: - out.append(body) + # Convert line-starting hyphens to asterisks for wiki lists. + wikitext = re.sub(r'^- ', '* ', raw_text, flags=re.MULTILINE) - if item_order: - out.append('') - out.append('= Items =') - for item in item_order: - out.append(f'== {{{{ItemIcon|{item}}}}} ==') - for body in item_sections[item]: - out.append(body) + if not sorted_names: + return wikitext - if hero_order: - out.append('') - out.append('= Heroes =') - for hero in hero_order: - out.append(f'== {{{{HeroIcon|{hero}}}}} ==') - section = hero_sections[hero] + # 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 body in section['general']: - out.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) - for ability in section['ability_order']: - out.append(f'==== {{{{AbilityIcon|{ability}}}}} ====') - for body in section['abilities'][ability]: - out.append(body) + # Apply the replacement across the entire text. + wikitext = pattern.sub(replace_with_template, wikitext) - return out + return wikitext From 26fe3d9e524878d48ad98b53485533b35e248ea6 Mon Sep 17 00:00:00 2001 From: LVL1024 <70866179+LVL1024@users.noreply.github.com> Date: Fri, 3 Jul 2026 21:12:12 +0000 Subject: [PATCH 10/14] chore: regenerate poetry.lock with Poetry 2.1.4 format --- poetry.lock | 49 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/poetry.lock b/poetry.lock index 9cce83f3..0ae762f5 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.4 and should not be changed by hand. [[package]] name = "certifi" @@ -6,6 +6,7 @@ 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.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db"}, {file = "certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432"}, @@ -17,6 +18,7 @@ version = "3.5.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.10" +groups = ["dev"] files = [ {file = "cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0"}, {file = "cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132"}, @@ -28,6 +30,7 @@ version = "3.4.7" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d"}, {file = "charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8"}, @@ -166,6 +169,8 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main"] +markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -177,6 +182,7 @@ version = "0.4.3" description = "Distribution utilities" optional = false python-versions = "*" +groups = ["dev"] files = [ {file = "distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b"}, {file = "distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed"}, @@ -188,6 +194,7 @@ version = "3.29.5" description = "A platform independent file lock." optional = false python-versions = ">=3.10" +groups = ["dev"] files = [ {file = "filelock-3.29.5-py3-none-any.whl", hash = "sha256:8af830889ba3a0ffcefbd6c7d2af8a54012058103771f2e10848222f476a1693"}, {file = "filelock-3.29.5.tar.gz", hash = "sha256:6e6034c57a00a020e767f2614a5539863f056de7e7991d6d1473aef7ff73f156"}, @@ -199,6 +206,7 @@ version = "2.6.19" description = "File identification library for Python" optional = false python-versions = ">=3.10" +groups = ["dev"] files = [ {file = "identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a"}, {file = "identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842"}, @@ -213,6 +221,7 @@ version = "3.18" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2"}, {file = "idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848"}, @@ -227,6 +236,7 @@ version = "0.1" description = "Read and write Valve's KeyValues3 format" optional = false python-versions = ">=3.11" +groups = ["main"] files = [ {file = "keyvalues3-0.1-py3-none-any.whl", hash = "sha256:0a20354d3fcab856d84386bddf186e29b113a785a7253dfee705494078d20539"}, {file = "keyvalues3-0.1.tar.gz", hash = "sha256:37b894c71cfa071758895401f833d0ccacd5fdb99063a72681ab6079a674c2a1"}, @@ -245,6 +255,7 @@ version = "0.7.3" description = "Python logging made (stupidly) simple" optional = false python-versions = "<4.0,>=3.5" +groups = ["main"] files = [ {file = "loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c"}, {file = "loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6"}, @@ -255,7 +266,7 @@ colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} [package.extras] -dev = ["Sphinx (==8.1.3)", "build (==1.2.2)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.5.0)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.13.0)", "mypy (==v1.4.1)", "myst-parser (==4.0.0)", "pre-commit (==4.0.1)", "pytest (==6.1.2)", "pytest (==8.3.2)", "pytest-cov (==2.12.1)", "pytest-cov (==5.0.0)", "pytest-cov (==6.0.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.1.0)", "sphinx-rtd-theme (==3.0.2)", "tox (==3.27.1)", "tox (==4.23.2)", "twine (==6.0.1)"] +dev = ["Sphinx (==8.1.3) ; python_version >= \"3.11\"", "build (==1.2.2) ; python_version >= \"3.11\"", "colorama (==0.4.5) ; python_version < \"3.8\"", "colorama (==0.4.6) ; python_version >= \"3.8\"", "exceptiongroup (==1.1.3) ; python_version >= \"3.7\" and python_version < \"3.11\"", "freezegun (==1.1.0) ; python_version < \"3.8\"", "freezegun (==1.5.0) ; python_version >= \"3.8\"", "mypy (==v0.910) ; python_version < \"3.6\"", "mypy (==v0.971) ; python_version == \"3.6\"", "mypy (==v1.13.0) ; python_version >= \"3.8\"", "mypy (==v1.4.1) ; python_version == \"3.7\"", "myst-parser (==4.0.0) ; python_version >= \"3.11\"", "pre-commit (==4.0.1) ; python_version >= \"3.9\"", "pytest (==6.1.2) ; python_version < \"3.8\"", "pytest (==8.3.2) ; python_version >= \"3.8\"", "pytest-cov (==2.12.1) ; python_version < \"3.8\"", "pytest-cov (==5.0.0) ; python_version == \"3.8\"", "pytest-cov (==6.0.0) ; python_version >= \"3.9\"", "pytest-mypy-plugins (==1.9.3) ; python_version >= \"3.6\" and python_version < \"3.8\"", "pytest-mypy-plugins (==3.1.0) ; python_version >= \"3.8\"", "sphinx-rtd-theme (==3.0.2) ; python_version >= \"3.11\"", "tox (==3.27.1) ; python_version < \"3.8\"", "tox (==4.23.2) ; python_version >= \"3.8\"", "twine (==6.0.1) ; python_version >= \"3.11\""] [[package]] name = "lz4" @@ -263,6 +274,7 @@ version = "4.4.5" description = "LZ4 Bindings for Python" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "lz4-4.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d221fa421b389ab2345640a508db57da36947a437dfe31aeddb8d5c7b646c22d"}, {file = "lz4-4.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:7dc1e1e2dbd872f8fae529acd5e4839efd0b141eaa8ae7ce835a9fe80fbad89f"}, @@ -334,6 +346,7 @@ version = "0.11.0" description = "MediaWiki API client" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "mwclient-0.11.0-py3-none-any.whl", hash = "sha256:27914a307cb4539fd49a16d634c9c777c0de0976b3af9225bd95d4657bfd4d74"}, {file = "mwclient-0.11.0.tar.gz", hash = "sha256:2bb7696f3703243eb33514ab162d7f6076dcedd011be90682936046c5e34fd06"}, @@ -348,6 +361,7 @@ version = "1.10.0" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] files = [ {file = "nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827"}, {file = "nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb"}, @@ -359,6 +373,7 @@ 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.3.tar.gz", hash = "sha256:838ff8899dc2f0b652d4fcf6c5d7466cb7ad5abcb005668ac622d1e40f4d8a8d"}, ] @@ -376,6 +391,7 @@ version = "3.3.1" description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1"}, {file = "oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9"}, @@ -392,6 +408,7 @@ version = "0.11.0" description = "(Soon to be) the fastest pure-Python PEG parser I could muster" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "parsimonious-0.11.0-py3-none-any.whl", hash = "sha256:32e3818abf9f05b3b9f3b6d87d128645e30177e91f614d2277d88a0aea98fae2"}, {file = "parsimonious-0.11.0.tar.gz", hash = "sha256:e080377d98957beec053580d38ae54fcdf7c470fb78670ba4bf8b5f9d5cad2a9"}, @@ -409,6 +426,7 @@ version = "12.3.0" description = "Python Imaging Library (fork)" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {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"}, @@ -513,6 +531,7 @@ version = "4.10.0" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.10" +groups = ["dev"] files = [ {file = "platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a"}, {file = "platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7"}, @@ -524,6 +543,7 @@ version = "3.8.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, @@ -542,6 +562,7 @@ version = "1.4.3" description = "Python interpreter discovery" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "python_discovery-1.4.3-py3-none-any.whl", hash = "sha256:b6e1e4a7d9e3f6948c39746ffe8218225162d738ba39d05ab1d2f6c1cac4878c"}, {file = "python_discovery-1.4.3.tar.gz", hash = "sha256:ad57d7045a862460d4a235986c33f13ed707d3aeb9153fa47eb7dfd0d4673289"}, @@ -561,6 +582,7 @@ version = "1.2.2" description = "Read key-value pairs from a .env file and set them as environment variables" optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a"}, {file = "python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3"}, @@ -575,6 +597,7 @@ version = "0.1.3" description = "A (quite) simple that helps creating on-the-fly Mermaid diagrams" optional = false python-versions = ">=3.10,<4.0" +groups = ["main"] files = [ {file = "python_mermaid-0.1.3-py3-none-any.whl", hash = "sha256:7d283d98b21b97feb428593905fb8b35e4cd8dbd038aa3ef1e09118d4a91ea89"}, {file = "python_mermaid-0.1.3.tar.gz", hash = "sha256:8c9cd781fdfc8ed958513782a982254037913f3d1d893018691044bafa3bd441"}, @@ -589,6 +612,7 @@ version = "6.0.3" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, @@ -671,6 +695,7 @@ version = "2026.6.28" description = "Alternative regular expression module, to replace re." optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {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"}, @@ -794,6 +819,7 @@ version = "2.34.2" description = "Python HTTP for Humans." optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0"}, {file = "requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed"}, @@ -815,6 +841,7 @@ version = "2.0.0" description = "OAuthlib authentication support for Requests." optional = false python-versions = ">=3.4" +groups = ["main"] files = [ {file = "requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9"}, {file = "requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36"}, @@ -833,6 +860,7 @@ version = "0.6.9" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "ruff-0.6.9-py3-none-linux_armv6l.whl", hash = "sha256:064df58d84ccc0ac0fcd63bc3090b251d90e2a372558c0f057c3f75ed73e1ccd"}, {file = "ruff-0.6.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:140d4b5c9f5fc7a7b074908a78ab8d384dd7f6510402267bc76c37195c02a7ec"}, @@ -860,6 +888,7 @@ version = "2024.2" description = "Provider of IANA time zone data" optional = false python-versions = ">=2" +groups = ["main"] files = [ {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"}, {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"}, @@ -871,6 +900,7 @@ version = "1.4.0" description = "ASCII transliterations of Unicode text" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "Unidecode-1.4.0-py3-none-any.whl", hash = "sha256:c3c7606c27503ad8d501270406e345ddb480a7b5f38827eafe4fa82a137f0021"}, {file = "Unidecode-1.4.0.tar.gz", hash = "sha256:ce35985008338b676573023acc382d62c264f307c8f7963733405add37ea2b23"}, @@ -882,16 +912,17 @@ version = "2.7.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.10" +groups = ["main"] files = [ {file = "urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897"}, {file = "urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c"}, ] [package.extras] -brotli = ["brotli (>=1.2.0)", "brotlicffi (>=1.2.0.0)"] +brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["backports-zstd (>=1.0.0)"] +zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] [[package]] name = "virtualenv" @@ -899,6 +930,7 @@ version = "21.5.1" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.9" +groups = ["dev"] files = [ {file = "virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783"}, {file = "virtualenv-21.5.1.tar.gz", hash = "sha256:dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8"}, @@ -916,13 +948,15 @@ version = "1.2.0" description = "A small Python utility to set file creation time on Windows" optional = false python-versions = ">=3.5" +groups = ["main"] +markers = "sys_platform == \"win32\"" files = [ {file = "win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390"}, {file = "win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0"}, ] [package.extras] -dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] +dev = ["black (>=19.3b0) ; python_version >= \"3.6\"", "pytest (>=4.6.2)"] [[package]] name = "zstandard" @@ -930,6 +964,7 @@ version = "0.25.0" description = "Zstandard bindings for Python" optional = false python-versions = ">=3.9" +groups = ["main"] files = [ {file = "zstandard-0.25.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e59fdc271772f6686e01e1b3b74537259800f57e24280be3f29c8a0deb1904dd"}, {file = "zstandard-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4d441506e9b372386a5271c64125f72d5df6d2a8e8a2a45a0ae09b03cb781ef7"}, @@ -1033,9 +1068,9 @@ files = [ ] [package.extras] -cffi = ["cffi (>=1.17,<2.0)", "cffi (>=2.0.0b)"] +cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and python_version < \"3.14\"", "cffi (>=2.0.0b) ; platform_python_implementation != \"PyPy\" and python_version >= \"3.14\""] [metadata] -lock-version = "2.0" +lock-version = "2.1" python-versions = ">=3.11,<3.15" content-hash = "98215fe1e749b03338f96fc7994319c71b530765c4d0711672f2e83c25f163db" From 7f2e1446a4af494388672289c4ec03c777f9c3dd Mon Sep 17 00:00:00 2001 From: LVL1024 <70866179+LVL1024@users.noreply.github.com> Date: Sat, 4 Jul 2026 17:25:32 +0000 Subject: [PATCH 11/14] feat: enhance changelog processing with pending ID support and wiki updates fetching --- src/changelogs/fetch_changelogs.py | 59 +++++++++++++--- src/changelogs/parse_changelogs.py | 11 ++- src/deadbot.py | 20 ++++-- src/wiki/changelog_utils.py | 62 ++++++++++++++++- src/wiki/upload.py | 107 ++++++++++++++--------------- 5 files changed, 189 insertions(+), 70 deletions(-) diff --git a/src/changelogs/fetch_changelogs.py b/src/changelogs/fetch_changelogs.py index 942c847b..e095274f 100644 --- a/src/changelogs/fetch_changelogs.py +++ b/src/changelogs/fetch_changelogs.py @@ -3,7 +3,7 @@ import requests import hashlib from utils import file_utils, json_utils -from typing import TypedDict, NotRequired +from typing import TypedDict, NotRequired, Set, Optional from .constants import STEAM_APP_ID, STEAM_NEWS_API_URL, STEAM_MIGRATION_DATE from collections import defaultdict from datetime import datetime @@ -55,7 +55,7 @@ class ChangelogFetcher: Fetches changelogs from Steam Web API and game files and parses them into a dictionary """ - def __init__(self, update_existing, input_dir, output_dir): + def __init__(self, update_existing, input_dir, output_dir, existing_dates: Optional[Set[str]] = None): self.changelogs: dict[str, ChangelogString] = {} self.changelog_configs: dict[str, ChangelogConfig] = {} self.hotfixes: list[Hotfix] = [] @@ -71,6 +71,14 @@ def __init__(self, update_existing, input_dir, output_dir): self.wiki_site = None + # Track which changelog_ids need full reprocessing this run. + # Preliminary set: entries whose date is not yet on the wiki, or that + # have was_edited set. After Steam fetch, recently_fetched_ids are + # merged in to cover same-day hotfix landings on already-uploaded dates. + self.existing_dates: Set[str] = existing_dates or set() + self.pending_prelim: Set[str] = set() + self.recently_fetched_ids: Set[str] = set() + self._load_input_data() def _load_input_data(self): @@ -79,12 +87,25 @@ def _load_input_data(self): existing_changelogs = json_utils.read(path) self.changelog_configs = existing_changelogs - # load 'changelogs/raw/.txt' files - all_files = os.listdir(f'{self.INPUT_DIR}/changelogs/raw') - for file in all_files: - raw_changelog = file_utils.read(f'{self.INPUT_DIR}/changelogs/raw/{file}') - changelog_id = file.replace('.txt', '') - self.changelogs[changelog_id] = raw_changelog + # Determine which entries need reprocessing: dates not yet on the wiki + # plus any entry flagged was_edited (keeps surfacing until manually resolved). + self.pending_prelim = set() + for cid, config in self.changelog_configs.items(): + cid_date = config.get('date', '') + if cid_date not in self.existing_dates or config.get('was_edited'): + self.pending_prelim.add(cid) + + # Only read raw txt files for changelog_ids in the pending set. + # Full history of raw text is not preloaded into memory. + raw_dir = f'{self.INPUT_DIR}/changelogs/raw' + try: + for file in os.listdir(raw_dir): + changelog_id = file.replace('.txt', '') + if changelog_id in self.pending_prelim: + raw_changelog = file_utils.read(f'{raw_dir}/{file}') + self.changelogs[changelog_id] = raw_changelog + except FileNotFoundError: + pass def _get_wiki_content(self, date_key: str) -> str: """ @@ -196,6 +217,11 @@ def changelogs_to_file(self): Since output data is paved over each deploy, we need this source for historic changelog data. + + Only writes raw txt files back out for changelog_ids in the pending set + (entries whose date is not yet on the wiki, was_edited, or just fetched + from Steam). Full history of changelog_configs.json is always written + since it is small metadata. """ # Sort the keys by the date lexicographically @@ -207,7 +233,13 @@ def changelogs_to_file(self): raw_output_dir = os.path.join(self.OUTPUT_DIR, 'changelogs/raw') raw_input_dir = os.path.join(self.INPUT_DIR, 'changelogs/raw') + # Merge Steam-fetched IDs into the pending set so that same-day hotfixes + # landing on an already-uploaded date still get their files written. + final_pending = self.pending_prelim | self.recently_fetched_ids + for changelog_id, changelog in self.changelogs.items(): + if changelog_id not in final_pending: + continue os.makedirs(raw_output_dir, exist_ok=True) file_utils.write(f'{raw_output_dir}/{changelog_id}.txt', changelog) @@ -219,6 +251,16 @@ def changelogs_to_file(self): # Save decoupled hotfixes for the uploader json_utils.write(f'{self.OUTPUT_DIR}/changelogs/hotfixes.json', self.hotfixes) + def get_pending_changelog_ids(self) -> Set[str]: + """ + Return the set of changelog_ids that need full processing this run. + This is the union of: + - Entries whose date is not yet live on the wiki (from pending_prelim) + - Entries the Steam fetch returned data for this run (recently_fetched_ids) + - Entries flagged was_edited (already included via pending_prelim) + """ + return self.pending_prelim | self.recently_fetched_ids + def fetch_steam_changelogs(self): """Fetch patch notes from Steam Web API.""" logger.trace('Fetching Steam news for changelogs') @@ -267,6 +309,7 @@ def fetch_steam_changelogs(self): for date_key, entries in updates_by_day.items(): changelog_id = date_key + self.recently_fetched_ids.add(changelog_id) 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 '' diff --git a/src/changelogs/parse_changelogs.py b/src/changelogs/parse_changelogs.py index 8023b9af..b69d779c 100644 --- a/src/changelogs/parse_changelogs.py +++ b/src/changelogs/parse_changelogs.py @@ -24,8 +24,11 @@ def __init__(self, output_dir): self.tags = Tags(self.default_tag, self.OUTPUT_CHANGELOGS) # Main - def run_all(self, dict_changelogs): + def run_all(self, dict_changelogs, pending_ids=None): for version, changelog in dict_changelogs.items(): + # Only process pending entries when a pending set is provided. + if pending_ids is not None and version not in pending_ids: + continue self.run(version, changelog) # Parse a single changelog file @@ -62,9 +65,10 @@ def run(self, version, logs): os.makedirs(self.OUTPUT_CHANGELOGS, exist_ok=True) # json_utils.write(self.OUTPUT_CHANGELOGS + f'/versions/{version}.json', changelog_with_icons) - def format_and_save_wikitext_changelogs(self, changelogs, changelog_configs): + def format_and_save_wikitext_changelogs(self, changelogs, changelog_configs, pending_ids=None): """ Formats changelogs into wikitext and saves them to files for later upload. + Only processes changelog_ids in pending_ids when provided. """ try: # Load data required for formatting entity names. @@ -98,6 +102,9 @@ def format_and_save_wikitext_changelogs(self, changelogs, changelog_configs): ) for changelog_id, raw_text in changelogs.items(): + # Only process pending entries when a pending set is provided. + if pending_ids is not None and changelog_id not in pending_ids: + continue config = changelog_configs.get(changelog_id) if not config or config.get('is_hero_lab'): continue diff --git a/src/deadbot.py b/src/deadbot.py index 28bf0c18..d94d6a1e 100644 --- a/src/deadbot.py +++ b/src/deadbot.py @@ -14,6 +14,7 @@ from utils.parameters import load_arguments, Args from utils.process import run_process from wiki.upload import WikiUpload +from wiki.changelog_utils import fetch_existing_wiki_updates load_dotenv() @@ -81,13 +82,15 @@ def main(): if args.changelogs: logger.info('Parsing Changelogs...') - act_changelog_parse(args) + _, pending_ids, wiki_updates = act_changelog_parse(args) else: + pending_ids = None + wiki_updates = None logger.trace('! Skipping Changelogs !') if args.wiki_upload: logger.info('Running Wiki Upload...') - wiki_upload = WikiUpload(args.output, dry_run=args.dry_run) + wiki_upload = WikiUpload(args.output, dry_run=args.dry_run, pending_changelog_ids=pending_ids, wiki_updates=wiki_updates) wiki_upload.run() else: logger.trace('! Skipping Wiki Upload !') @@ -104,20 +107,29 @@ def act_gamefile_parse(args: Args): def act_changelog_parse(args: Args): + # Single wiki query at the start — fetches both the date set (for pending + # computation) and the full (date, title) tuple list (for upload linking). + # Both ChangelogFetcher and WikiUpload receive these, avoiding redundant scans. + existing_dates, wiki_updates = fetch_existing_wiki_updates() + chlog_fetcher = fetch_changelogs.ChangelogFetcher( update_existing=False, input_dir=args.inputdir, output_dir=args.output, + existing_dates=existing_dates, ) chlog_fetcher.run() + pending_ids = chlog_fetcher.get_pending_changelog_ids() + chlog_parser = parse_changelogs.ChangelogParser(args.output) - chlog_parser.run_all(chlog_fetcher.changelogs) + chlog_parser.run_all(chlog_fetcher.changelogs, pending_ids=pending_ids) chlog_parser.format_and_save_wikitext_changelogs( chlog_fetcher.changelogs, chlog_fetcher.changelog_configs, + pending_ids=pending_ids, ) - return chlog_parser + return chlog_parser, pending_ids, wiki_updates if __name__ == '__main__': diff --git a/src/wiki/changelog_utils.py b/src/wiki/changelog_utils.py index bc2f0cc6..bbd85269 100644 --- a/src/wiki/changelog_utils.py +++ b/src/wiki/changelog_utils.py @@ -1,6 +1,66 @@ import re from datetime import datetime -from typing import List, Tuple, Optional +from typing import List, Tuple, Optional, Set +import mwclient +from loguru import logger + + +def fetch_existing_wiki_updates(site: mwclient.Site = None) -> Tuple[Set[str], List[Tuple[datetime, str]]]: + """ + Single bulk allpages traversal of the wiki's Update namespace. + Returns both the set of YYYY-MM-DD dates and the full (date, title) tuples + so callers that need both avoid redundant scans. + + Args: + site: An authenticated or anonymous mwclient.Site for deadlock.wiki. + If None, a read-only anonymous connection is created. + + Returns: + Tuple of (existing_dates, wiki_updates) where: + existing_dates: Set of date strings in 'YYYY-MM-DD' format + wiki_updates: List of (datetime, page_title) tuples + """ + existing_dates: Set[str] = set() + wiki_updates: List[Tuple[datetime, str]] = [] + try: + if site is None: + site = mwclient.Site('deadlock.wiki', path='/') + namespace_id = _get_namespace_id(site, 'Update') + pages = site.allpages(namespace=namespace_id) + + for page in pages: + if ':' not in page.name: + continue + title_part = page.name.split(':', 1)[1] + if '/' in title_part: + title_part = title_part.split('/', 1)[0] + normalized_title = title_part.replace('_', ' ') + try: + date_obj = datetime.strptime(normalized_title, '%B %d, %Y') + existing_dates.add(date_obj.strftime('%Y-%m-%d')) + wiki_updates.append((date_obj, page.name)) + except ValueError: + continue + + except Exception as e: + logger.warning(f'Failed to query wiki for existing update pages: {e}') + return set(), [] + + return existing_dates, wiki_updates + + +def get_existing_wiki_update_dates(site: mwclient.Site = None) -> Set[str]: + """Convenience wrapper around fetch_existing_wiki_updates that returns only the date set.""" + dates, _ = fetch_existing_wiki_updates(site) + return dates + + +def _get_namespace_id(site: mwclient.Site, search_namespace: str) -> int: + """Look up a namespace ID by its name.""" + for namespace_id, namespace in site.namespaces.items(): + if namespace == search_namespace: + return namespace_id + raise ValueError(f'Namespace {search_namespace} not found') def sort_changelog_files(files: List[str]) -> List[str]: diff --git a/src/wiki/upload.py b/src/wiki/upload.py index 0a1c14a5..aaf42b89 100644 --- a/src/wiki/upload.py +++ b/src/wiki/upload.py @@ -3,7 +3,7 @@ import json import re from datetime import datetime -from typing import List, Tuple +from typing import List, Tuple, Optional, Set from utils import json_utils, game_utils, meta_utils from .pages import DATA_PAGE_FILE_MAP, IGNORE_PAGES from loguru import logger @@ -15,9 +15,17 @@ class WikiUpload: Uploads a set of specified data to deadlock.wiki via the MediaWiki API """ - def __init__(self, output_dir, dry_run=False): + def __init__( + self, + output_dir, + dry_run=False, + pending_changelog_ids: Optional[Set[str]] = None, + wiki_updates: Optional[List[Tuple[datetime, str]]] = None, + ): self.OUTPUT_DIR = output_dir self.DATA_NAMESPACE = 'Data' + # Set of changelog_ids that need processing this run; if None, process all. + self.pending_changelog_ids = pending_changelog_ids if dry_run: logger.info('Wiki upload is running in dry-run mode. No changes will be made to the wiki.') @@ -43,13 +51,15 @@ def __init__(self, output_dir, dry_run=False): self.site.login(self.auth['user'], self.auth['password']) - # Store wiki state to minimize API calls - self.wiki_updates: List[Tuple[datetime, str]] = [] + # Store wiki state; use pre-fetched from caller if provided to avoid + # a redundant allpages scan (deadbot.py queries once and distributes). + self.wiki_updates: List[Tuple[datetime, str]] = wiki_updates or [] def run(self): logger.info(f'Uploading Data to Wiki - {self.upload_message}') self._update_data_pages() - self.wiki_updates = self._get_existing_update_pages() + if not self.wiki_updates: + self.wiki_updates = self._get_existing_update_pages() self._upload_changelog_pages() self._process_hotfixes() self._update_latest_chain() @@ -57,48 +67,17 @@ def run(self): def _get_existing_update_pages(self) -> List[Tuple[datetime, str]]: """ Fetch all Update pages from wiki and return list of (date, title) tuples. - Simplified by replacing underscores with spaces. + Uses the shared single-scan helper in changelog_utils. """ - update_pages = [] - try: - namespace_id = self._get_namespace_id('Update') - pages = self.site.allpages(namespace=namespace_id) - count = 0 - - for page in pages: - count += 1 - if ':' not in page.name: - continue - - # Parse title part after "Update:" - title_part = page.name.split(':', 1)[1] - - # Remove any subpage suffix (e.g., "/ru") - if '/' in title_part: - title_part = title_part.split('/', 1)[0] - - # Replace underscores with spaces to unify format (Maintainer suggestion) - normalized_title = title_part.replace('_', ' ') - - try: - date_obj = datetime.strptime(normalized_title, '%B %d, %Y') - update_pages.append((date_obj, page.name)) - except ValueError: - logger.trace(f'Skipping page with non-date title: {page.name}') - continue - - logger.debug(f'Scanned {count} pages in Update namespace, found {len(update_pages)} valid update pages') - - except Exception as e: - logger.error(f'Failed to fetch existing update pages: {e}') - return [] - - return update_pages + _, wiki_updates = changelog_utils.fetch_existing_wiki_updates(self.site) + logger.debug(f'Found {len(wiki_updates)} valid update pages in Update namespace') + return wiki_updates def _upload_changelog_pages(self): """ Reads formatted changelog files and uploads them to the wiki. Queries wiki state to ensure correct prev_update linking on first upload. + Only processes changelog_ids in self.pending_changelog_ids (if set). """ changelog_dir = os.path.join(self.OUTPUT_DIR, 'changelogs', 'wiki') if not os.path.isdir(changelog_dir): @@ -111,16 +90,27 @@ def _upload_changelog_pages(self): changelog_configs_path = os.path.join(self.OUTPUT_DIR, 'changelogs', 'changelog_configs.json') changelog_configs = json_utils.read(changelog_configs_path, ignore_error=True) or {} - # Get and sort files with proper variant ordering via utility - files = changelog_utils.sort_changelog_files([f for f in os.listdir(changelog_dir) if f.endswith('.txt')]) - if not files: + # Determine which changelog_ids to process this run. + # Only process pending ids when the set is provided; otherwise fall back + # to scanning the directory (backward compat). + if self.pending_changelog_ids is not None: + # Sort chronologically by date+variant so intra-run prev_update linking works. + candidate_ids = [ + f.replace('.txt', '') for f in changelog_utils.sort_changelog_files([f'{cid}.txt' for cid in self.pending_changelog_ids]) + ] + else: + candidate_ids = [ + f.replace('.txt', '') for f in changelog_utils.sort_changelog_files([f for f in os.listdir(changelog_dir) if f.endswith('.txt')]) + ] + + if not candidate_ids: return # Track uploads in this run for correct intra-run linking uploads_this_run: List[Tuple[datetime, str]] = [] - for filename in files: - changelog_id = filename.replace('.txt', '') + for changelog_id in candidate_ids: + filename = f'{changelog_id}.txt' # Skip if the post was edited by devs to protect manual wiki edits if changelog_configs.get(changelog_id, {}).get('was_edited'): @@ -135,6 +125,9 @@ def _upload_changelog_pages(self): page_title = f'Update:{wiki_date_str}' filepath = os.path.join(changelog_dir, filename) + if not os.path.exists(filepath): + logger.trace(f'Wikitext file not found for pending changelog {changelog_id}, skipping.') + continue with open(filepath, 'r', encoding='utf-8') as f: raw_content = f.read() @@ -252,23 +245,27 @@ def _update_latest_chain(self): Queries the wiki to find the most recent update page that is strictly earlier than the current patch and links it forward. This ensures the chain is correctly maintained even if intermediate update pages were created manually on the wiki. + + Determines the latest changelog by reading changelog_configs.json (which holds full + history) rather than scanning wikitext files in the output folder. """ - changelog_dir = os.path.join(self.OUTPUT_DIR, 'changelogs', 'wiki') - if not os.path.isdir(changelog_dir): + # Read changelog configs to find the latest entry (holds full history metadata) + changelog_configs_path = os.path.join(self.OUTPUT_DIR, 'changelogs', 'changelog_configs.json') + changelog_configs = json_utils.read(changelog_configs_path, ignore_error=True) + if not changelog_configs: return - # Get all changelog files sorted by date - files = changelog_utils.sort_changelog_files([f for f in os.listdir(changelog_dir) if f.endswith('.txt')]) - if not files: + # Filter out hero lab entries and entries without dates, then sort by date+variant + update_ids = [f'{cid}.txt' for cid, cfg in changelog_configs.items() if cfg.get('date') and not cfg.get('is_hero_lab')] + sorted_ids = changelog_utils.sort_changelog_files(update_ids) + if not sorted_ids: return - # Get the latest file being processed now - latest_file = files[-1] - latest_id = latest_file.replace('.txt', '') + latest_id = sorted_ids[-1].replace('.txt', '') latest_date = changelog_utils.parse_changelog_date_from_id(latest_id) if not latest_date: - logger.error(f'Could not parse date from {latest_file}, cannot update chain') + logger.error(f'Could not parse date from {latest_id}, cannot update chain') return latest_page_title = f"Update:{latest_date.strftime('%B')}_{latest_date.day},_{latest_date.year}" From 9b8de0e208d6c910afab4b0be39b312596384fd2 Mon Sep 17 00:00:00 2001 From: LVL1024 <70866179+LVL1024@users.noreply.github.com> Date: Sat, 4 Jul 2026 17:27:56 +0000 Subject: [PATCH 12/14] TEST: Delete existing notes --- input-data/changelogs/changelog_configs.json | 32 ---- input-data/changelogs/raw/2026-06-04.txt | 18 -- input-data/changelogs/raw/2026-06-11.txt | 105 ----------- input-data/changelogs/raw/2026-06-30.txt | 187 ------------------- input-data/changelogs/raw/2026-07-01.txt | 5 - 5 files changed, 347 deletions(-) delete mode 100644 input-data/changelogs/raw/2026-06-04.txt delete mode 100644 input-data/changelogs/raw/2026-06-11.txt delete mode 100644 input-data/changelogs/raw/2026-06-30.txt delete mode 100644 input-data/changelogs/raw/2026-07-01.txt diff --git a/input-data/changelogs/changelog_configs.json b/input-data/changelogs/changelog_configs.json index 177eccd5..d433e5de 100644 --- a/input-data/changelogs/changelog_configs.json +++ b/input-data/changelogs/changelog_configs.json @@ -760,37 +760,5 @@ "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" } } \ 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 deleted file mode 100644 index 3a89127d..00000000 --- a/input-data/changelogs/raw/2026-06-04.txt +++ /dev/null @@ -1,18 +0,0 @@ -- 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 deleted file mode 100644 index bd71b0cf..00000000 --- a/input-data/changelogs/raw/2026-06-11.txt +++ /dev/null @@ -1,105 +0,0 @@ -- 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 deleted file mode 100644 index c0eb4076..00000000 --- a/input-data/changelogs/raw/2026-06-30.txt +++ /dev/null @@ -1,187 +0,0 @@ -[ 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 deleted file mode 100644 index 1523f88b..00000000 --- a/input-data/changelogs/raw/2026-07-01.txt +++ /dev/null @@ -1,5 +0,0 @@ -- 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 From b1994dc99edd0aeb6abdff083abbe42b07d57e54 Mon Sep 17 00:00:00 2001 From: LVL1024 <70866179+LVL1024@users.noreply.github.com> Date: Sat, 4 Jul 2026 17:34:58 +0000 Subject: [PATCH 13/14] feat: exclude Hero Lab entries from reprocessing in changelog fetcher --- src/changelogs/fetch_changelogs.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/changelogs/fetch_changelogs.py b/src/changelogs/fetch_changelogs.py index e095274f..bf5b8e3f 100644 --- a/src/changelogs/fetch_changelogs.py +++ b/src/changelogs/fetch_changelogs.py @@ -89,8 +89,13 @@ def _load_input_data(self): # Determine which entries need reprocessing: dates not yet on the wiki # plus any entry flagged was_edited (keeps surfacing until manually resolved). + # Hero Lab entries are excluded -- they have non-standard ID formats + # (e.g. "2026-06-24_HeroLab") that sort_changelog_files can't parse, + # and they are already skipped during wikitext formatting. self.pending_prelim = set() for cid, config in self.changelog_configs.items(): + if config.get('is_hero_lab'): + continue cid_date = config.get('date', '') if cid_date not in self.existing_dates or config.get('was_edited'): self.pending_prelim.add(cid) From e20047ab56c51649228273d28ae842396ddebbf0 Mon Sep 17 00:00:00 2001 From: LVL1024 <70866179+LVL1024@users.noreply.github.com> Date: Sat, 4 Jul 2026 17:59:09 +0000 Subject: [PATCH 14/14] Revert changelog reprocessing changes, keep DRY_RUN docker fix Reverts the 3 commits since 26fe3d9e: - b1994dc feat: exclude Hero Lab entries from reprocessing - 9b8de0e TEST: Delete existing notes - 7f2e144 feat: enhance changelog processing with pending ID support Preserves the DRY_RUN env var forwarding fix in docker-compose.yml, which fixes a pre-existing bug where the variable was set by the CI workflow but silently dropped at the container boundary. --- docker-compose.yml | 1 + input-data/changelogs/changelog_configs.json | 32 ++++ input-data/changelogs/raw/2026-06-04.txt | 18 ++ input-data/changelogs/raw/2026-06-11.txt | 105 +++++++++++ input-data/changelogs/raw/2026-06-30.txt | 187 +++++++++++++++++++ input-data/changelogs/raw/2026-07-01.txt | 5 + src/changelogs/fetch_changelogs.py | 64 +------ src/changelogs/parse_changelogs.py | 11 +- src/deadbot.py | 20 +- src/wiki/changelog_utils.py | 62 +----- src/wiki/upload.py | 107 +++++------ 11 files changed, 418 insertions(+), 194 deletions(-) create mode 100644 input-data/changelogs/raw/2026-06-04.txt create mode 100644 input-data/changelogs/raw/2026-06-11.txt create mode 100644 input-data/changelogs/raw/2026-06-30.txt create mode 100644 input-data/changelogs/raw/2026-07-01.txt 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..177eccd5 100644 --- a/input-data/changelogs/changelog_configs.json +++ b/input-data/changelogs/changelog_configs.json @@ -760,5 +760,37 @@ "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" } } \ 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/src/changelogs/fetch_changelogs.py b/src/changelogs/fetch_changelogs.py index bf5b8e3f..942c847b 100644 --- a/src/changelogs/fetch_changelogs.py +++ b/src/changelogs/fetch_changelogs.py @@ -3,7 +3,7 @@ import requests import hashlib from utils import file_utils, json_utils -from typing import TypedDict, NotRequired, Set, Optional +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 @@ -55,7 +55,7 @@ class ChangelogFetcher: Fetches changelogs from Steam Web API and game files and parses them into a dictionary """ - def __init__(self, update_existing, input_dir, output_dir, existing_dates: Optional[Set[str]] = None): + def __init__(self, update_existing, input_dir, output_dir): self.changelogs: dict[str, ChangelogString] = {} self.changelog_configs: dict[str, ChangelogConfig] = {} self.hotfixes: list[Hotfix] = [] @@ -71,14 +71,6 @@ def __init__(self, update_existing, input_dir, output_dir, existing_dates: Optio self.wiki_site = None - # Track which changelog_ids need full reprocessing this run. - # Preliminary set: entries whose date is not yet on the wiki, or that - # have was_edited set. After Steam fetch, recently_fetched_ids are - # merged in to cover same-day hotfix landings on already-uploaded dates. - self.existing_dates: Set[str] = existing_dates or set() - self.pending_prelim: Set[str] = set() - self.recently_fetched_ids: Set[str] = set() - self._load_input_data() def _load_input_data(self): @@ -87,30 +79,12 @@ def _load_input_data(self): existing_changelogs = json_utils.read(path) self.changelog_configs = existing_changelogs - # Determine which entries need reprocessing: dates not yet on the wiki - # plus any entry flagged was_edited (keeps surfacing until manually resolved). - # Hero Lab entries are excluded -- they have non-standard ID formats - # (e.g. "2026-06-24_HeroLab") that sort_changelog_files can't parse, - # and they are already skipped during wikitext formatting. - self.pending_prelim = set() - for cid, config in self.changelog_configs.items(): - if config.get('is_hero_lab'): - continue - cid_date = config.get('date', '') - if cid_date not in self.existing_dates or config.get('was_edited'): - self.pending_prelim.add(cid) - - # Only read raw txt files for changelog_ids in the pending set. - # Full history of raw text is not preloaded into memory. - raw_dir = f'{self.INPUT_DIR}/changelogs/raw' - try: - for file in os.listdir(raw_dir): - changelog_id = file.replace('.txt', '') - if changelog_id in self.pending_prelim: - raw_changelog = file_utils.read(f'{raw_dir}/{file}') - self.changelogs[changelog_id] = raw_changelog - except FileNotFoundError: - pass + # load 'changelogs/raw/.txt' files + all_files = os.listdir(f'{self.INPUT_DIR}/changelogs/raw') + for file in all_files: + raw_changelog = file_utils.read(f'{self.INPUT_DIR}/changelogs/raw/{file}') + changelog_id = file.replace('.txt', '') + self.changelogs[changelog_id] = raw_changelog def _get_wiki_content(self, date_key: str) -> str: """ @@ -222,11 +196,6 @@ def changelogs_to_file(self): Since output data is paved over each deploy, we need this source for historic changelog data. - - Only writes raw txt files back out for changelog_ids in the pending set - (entries whose date is not yet on the wiki, was_edited, or just fetched - from Steam). Full history of changelog_configs.json is always written - since it is small metadata. """ # Sort the keys by the date lexicographically @@ -238,13 +207,7 @@ def changelogs_to_file(self): raw_output_dir = os.path.join(self.OUTPUT_DIR, 'changelogs/raw') raw_input_dir = os.path.join(self.INPUT_DIR, 'changelogs/raw') - # Merge Steam-fetched IDs into the pending set so that same-day hotfixes - # landing on an already-uploaded date still get their files written. - final_pending = self.pending_prelim | self.recently_fetched_ids - for changelog_id, changelog in self.changelogs.items(): - if changelog_id not in final_pending: - continue os.makedirs(raw_output_dir, exist_ok=True) file_utils.write(f'{raw_output_dir}/{changelog_id}.txt', changelog) @@ -256,16 +219,6 @@ def changelogs_to_file(self): # Save decoupled hotfixes for the uploader json_utils.write(f'{self.OUTPUT_DIR}/changelogs/hotfixes.json', self.hotfixes) - def get_pending_changelog_ids(self) -> Set[str]: - """ - Return the set of changelog_ids that need full processing this run. - This is the union of: - - Entries whose date is not yet live on the wiki (from pending_prelim) - - Entries the Steam fetch returned data for this run (recently_fetched_ids) - - Entries flagged was_edited (already included via pending_prelim) - """ - return self.pending_prelim | self.recently_fetched_ids - def fetch_steam_changelogs(self): """Fetch patch notes from Steam Web API.""" logger.trace('Fetching Steam news for changelogs') @@ -314,7 +267,6 @@ def fetch_steam_changelogs(self): for date_key, entries in updates_by_day.items(): changelog_id = date_key - self.recently_fetched_ids.add(changelog_id) 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 '' diff --git a/src/changelogs/parse_changelogs.py b/src/changelogs/parse_changelogs.py index b69d779c..8023b9af 100644 --- a/src/changelogs/parse_changelogs.py +++ b/src/changelogs/parse_changelogs.py @@ -24,11 +24,8 @@ def __init__(self, output_dir): self.tags = Tags(self.default_tag, self.OUTPUT_CHANGELOGS) # Main - def run_all(self, dict_changelogs, pending_ids=None): + def run_all(self, dict_changelogs): for version, changelog in dict_changelogs.items(): - # Only process pending entries when a pending set is provided. - if pending_ids is not None and version not in pending_ids: - continue self.run(version, changelog) # Parse a single changelog file @@ -65,10 +62,9 @@ def run(self, version, logs): os.makedirs(self.OUTPUT_CHANGELOGS, exist_ok=True) # json_utils.write(self.OUTPUT_CHANGELOGS + f'/versions/{version}.json', changelog_with_icons) - def format_and_save_wikitext_changelogs(self, changelogs, changelog_configs, pending_ids=None): + def format_and_save_wikitext_changelogs(self, changelogs, changelog_configs): """ Formats changelogs into wikitext and saves them to files for later upload. - Only processes changelog_ids in pending_ids when provided. """ try: # Load data required for formatting entity names. @@ -102,9 +98,6 @@ def format_and_save_wikitext_changelogs(self, changelogs, changelog_configs, pen ) for changelog_id, raw_text in changelogs.items(): - # Only process pending entries when a pending set is provided. - if pending_ids is not None and changelog_id not in pending_ids: - continue config = changelog_configs.get(changelog_id) if not config or config.get('is_hero_lab'): continue diff --git a/src/deadbot.py b/src/deadbot.py index d94d6a1e..28bf0c18 100644 --- a/src/deadbot.py +++ b/src/deadbot.py @@ -14,7 +14,6 @@ from utils.parameters import load_arguments, Args from utils.process import run_process from wiki.upload import WikiUpload -from wiki.changelog_utils import fetch_existing_wiki_updates load_dotenv() @@ -82,15 +81,13 @@ def main(): if args.changelogs: logger.info('Parsing Changelogs...') - _, pending_ids, wiki_updates = act_changelog_parse(args) + act_changelog_parse(args) else: - pending_ids = None - wiki_updates = None logger.trace('! Skipping Changelogs !') if args.wiki_upload: logger.info('Running Wiki Upload...') - wiki_upload = WikiUpload(args.output, dry_run=args.dry_run, pending_changelog_ids=pending_ids, wiki_updates=wiki_updates) + wiki_upload = WikiUpload(args.output, dry_run=args.dry_run) wiki_upload.run() else: logger.trace('! Skipping Wiki Upload !') @@ -107,29 +104,20 @@ def act_gamefile_parse(args: Args): def act_changelog_parse(args: Args): - # Single wiki query at the start — fetches both the date set (for pending - # computation) and the full (date, title) tuple list (for upload linking). - # Both ChangelogFetcher and WikiUpload receive these, avoiding redundant scans. - existing_dates, wiki_updates = fetch_existing_wiki_updates() - chlog_fetcher = fetch_changelogs.ChangelogFetcher( update_existing=False, input_dir=args.inputdir, output_dir=args.output, - existing_dates=existing_dates, ) chlog_fetcher.run() - pending_ids = chlog_fetcher.get_pending_changelog_ids() - chlog_parser = parse_changelogs.ChangelogParser(args.output) - chlog_parser.run_all(chlog_fetcher.changelogs, pending_ids=pending_ids) + chlog_parser.run_all(chlog_fetcher.changelogs) chlog_parser.format_and_save_wikitext_changelogs( chlog_fetcher.changelogs, chlog_fetcher.changelog_configs, - pending_ids=pending_ids, ) - return chlog_parser, pending_ids, wiki_updates + return chlog_parser if __name__ == '__main__': diff --git a/src/wiki/changelog_utils.py b/src/wiki/changelog_utils.py index bbd85269..bc2f0cc6 100644 --- a/src/wiki/changelog_utils.py +++ b/src/wiki/changelog_utils.py @@ -1,66 +1,6 @@ import re from datetime import datetime -from typing import List, Tuple, Optional, Set -import mwclient -from loguru import logger - - -def fetch_existing_wiki_updates(site: mwclient.Site = None) -> Tuple[Set[str], List[Tuple[datetime, str]]]: - """ - Single bulk allpages traversal of the wiki's Update namespace. - Returns both the set of YYYY-MM-DD dates and the full (date, title) tuples - so callers that need both avoid redundant scans. - - Args: - site: An authenticated or anonymous mwclient.Site for deadlock.wiki. - If None, a read-only anonymous connection is created. - - Returns: - Tuple of (existing_dates, wiki_updates) where: - existing_dates: Set of date strings in 'YYYY-MM-DD' format - wiki_updates: List of (datetime, page_title) tuples - """ - existing_dates: Set[str] = set() - wiki_updates: List[Tuple[datetime, str]] = [] - try: - if site is None: - site = mwclient.Site('deadlock.wiki', path='/') - namespace_id = _get_namespace_id(site, 'Update') - pages = site.allpages(namespace=namespace_id) - - for page in pages: - if ':' not in page.name: - continue - title_part = page.name.split(':', 1)[1] - if '/' in title_part: - title_part = title_part.split('/', 1)[0] - normalized_title = title_part.replace('_', ' ') - try: - date_obj = datetime.strptime(normalized_title, '%B %d, %Y') - existing_dates.add(date_obj.strftime('%Y-%m-%d')) - wiki_updates.append((date_obj, page.name)) - except ValueError: - continue - - except Exception as e: - logger.warning(f'Failed to query wiki for existing update pages: {e}') - return set(), [] - - return existing_dates, wiki_updates - - -def get_existing_wiki_update_dates(site: mwclient.Site = None) -> Set[str]: - """Convenience wrapper around fetch_existing_wiki_updates that returns only the date set.""" - dates, _ = fetch_existing_wiki_updates(site) - return dates - - -def _get_namespace_id(site: mwclient.Site, search_namespace: str) -> int: - """Look up a namespace ID by its name.""" - for namespace_id, namespace in site.namespaces.items(): - if namespace == search_namespace: - return namespace_id - raise ValueError(f'Namespace {search_namespace} not found') +from typing import List, Tuple, Optional def sort_changelog_files(files: List[str]) -> List[str]: diff --git a/src/wiki/upload.py b/src/wiki/upload.py index aaf42b89..0a1c14a5 100644 --- a/src/wiki/upload.py +++ b/src/wiki/upload.py @@ -3,7 +3,7 @@ import json import re from datetime import datetime -from typing import List, Tuple, Optional, Set +from typing import List, Tuple from utils import json_utils, game_utils, meta_utils from .pages import DATA_PAGE_FILE_MAP, IGNORE_PAGES from loguru import logger @@ -15,17 +15,9 @@ class WikiUpload: Uploads a set of specified data to deadlock.wiki via the MediaWiki API """ - def __init__( - self, - output_dir, - dry_run=False, - pending_changelog_ids: Optional[Set[str]] = None, - wiki_updates: Optional[List[Tuple[datetime, str]]] = None, - ): + def __init__(self, output_dir, dry_run=False): self.OUTPUT_DIR = output_dir self.DATA_NAMESPACE = 'Data' - # Set of changelog_ids that need processing this run; if None, process all. - self.pending_changelog_ids = pending_changelog_ids if dry_run: logger.info('Wiki upload is running in dry-run mode. No changes will be made to the wiki.') @@ -51,15 +43,13 @@ def __init__( self.site.login(self.auth['user'], self.auth['password']) - # Store wiki state; use pre-fetched from caller if provided to avoid - # a redundant allpages scan (deadbot.py queries once and distributes). - self.wiki_updates: List[Tuple[datetime, str]] = wiki_updates or [] + # Store wiki state to minimize API calls + self.wiki_updates: List[Tuple[datetime, str]] = [] def run(self): logger.info(f'Uploading Data to Wiki - {self.upload_message}') self._update_data_pages() - if not self.wiki_updates: - self.wiki_updates = self._get_existing_update_pages() + self.wiki_updates = self._get_existing_update_pages() self._upload_changelog_pages() self._process_hotfixes() self._update_latest_chain() @@ -67,17 +57,48 @@ def run(self): def _get_existing_update_pages(self) -> List[Tuple[datetime, str]]: """ Fetch all Update pages from wiki and return list of (date, title) tuples. - Uses the shared single-scan helper in changelog_utils. + Simplified by replacing underscores with spaces. """ - _, wiki_updates = changelog_utils.fetch_existing_wiki_updates(self.site) - logger.debug(f'Found {len(wiki_updates)} valid update pages in Update namespace') - return wiki_updates + update_pages = [] + try: + namespace_id = self._get_namespace_id('Update') + pages = self.site.allpages(namespace=namespace_id) + count = 0 + + for page in pages: + count += 1 + if ':' not in page.name: + continue + + # Parse title part after "Update:" + title_part = page.name.split(':', 1)[1] + + # Remove any subpage suffix (e.g., "/ru") + if '/' in title_part: + title_part = title_part.split('/', 1)[0] + + # Replace underscores with spaces to unify format (Maintainer suggestion) + normalized_title = title_part.replace('_', ' ') + + try: + date_obj = datetime.strptime(normalized_title, '%B %d, %Y') + update_pages.append((date_obj, page.name)) + except ValueError: + logger.trace(f'Skipping page with non-date title: {page.name}') + continue + + logger.debug(f'Scanned {count} pages in Update namespace, found {len(update_pages)} valid update pages') + + except Exception as e: + logger.error(f'Failed to fetch existing update pages: {e}') + return [] + + return update_pages def _upload_changelog_pages(self): """ Reads formatted changelog files and uploads them to the wiki. Queries wiki state to ensure correct prev_update linking on first upload. - Only processes changelog_ids in self.pending_changelog_ids (if set). """ changelog_dir = os.path.join(self.OUTPUT_DIR, 'changelogs', 'wiki') if not os.path.isdir(changelog_dir): @@ -90,27 +111,16 @@ def _upload_changelog_pages(self): changelog_configs_path = os.path.join(self.OUTPUT_DIR, 'changelogs', 'changelog_configs.json') changelog_configs = json_utils.read(changelog_configs_path, ignore_error=True) or {} - # Determine which changelog_ids to process this run. - # Only process pending ids when the set is provided; otherwise fall back - # to scanning the directory (backward compat). - if self.pending_changelog_ids is not None: - # Sort chronologically by date+variant so intra-run prev_update linking works. - candidate_ids = [ - f.replace('.txt', '') for f in changelog_utils.sort_changelog_files([f'{cid}.txt' for cid in self.pending_changelog_ids]) - ] - else: - candidate_ids = [ - f.replace('.txt', '') for f in changelog_utils.sort_changelog_files([f for f in os.listdir(changelog_dir) if f.endswith('.txt')]) - ] - - if not candidate_ids: + # Get and sort files with proper variant ordering via utility + files = changelog_utils.sort_changelog_files([f for f in os.listdir(changelog_dir) if f.endswith('.txt')]) + if not files: return # Track uploads in this run for correct intra-run linking uploads_this_run: List[Tuple[datetime, str]] = [] - for changelog_id in candidate_ids: - filename = f'{changelog_id}.txt' + for filename in files: + changelog_id = filename.replace('.txt', '') # Skip if the post was edited by devs to protect manual wiki edits if changelog_configs.get(changelog_id, {}).get('was_edited'): @@ -125,9 +135,6 @@ def _upload_changelog_pages(self): page_title = f'Update:{wiki_date_str}' filepath = os.path.join(changelog_dir, filename) - if not os.path.exists(filepath): - logger.trace(f'Wikitext file not found for pending changelog {changelog_id}, skipping.') - continue with open(filepath, 'r', encoding='utf-8') as f: raw_content = f.read() @@ -245,27 +252,23 @@ def _update_latest_chain(self): Queries the wiki to find the most recent update page that is strictly earlier than the current patch and links it forward. This ensures the chain is correctly maintained even if intermediate update pages were created manually on the wiki. - - Determines the latest changelog by reading changelog_configs.json (which holds full - history) rather than scanning wikitext files in the output folder. """ - # Read changelog configs to find the latest entry (holds full history metadata) - changelog_configs_path = os.path.join(self.OUTPUT_DIR, 'changelogs', 'changelog_configs.json') - changelog_configs = json_utils.read(changelog_configs_path, ignore_error=True) - if not changelog_configs: + changelog_dir = os.path.join(self.OUTPUT_DIR, 'changelogs', 'wiki') + if not os.path.isdir(changelog_dir): return - # Filter out hero lab entries and entries without dates, then sort by date+variant - update_ids = [f'{cid}.txt' for cid, cfg in changelog_configs.items() if cfg.get('date') and not cfg.get('is_hero_lab')] - sorted_ids = changelog_utils.sort_changelog_files(update_ids) - if not sorted_ids: + # Get all changelog files sorted by date + files = changelog_utils.sort_changelog_files([f for f in os.listdir(changelog_dir) if f.endswith('.txt')]) + if not files: return - latest_id = sorted_ids[-1].replace('.txt', '') + # Get the latest file being processed now + latest_file = files[-1] + latest_id = latest_file.replace('.txt', '') latest_date = changelog_utils.parse_changelog_date_from_id(latest_id) if not latest_date: - logger.error(f'Could not parse date from {latest_id}, cannot update chain') + logger.error(f'Could not parse date from {latest_file}, cannot update chain') return latest_page_title = f"Update:{latest_date.strftime('%B')}_{latest_date.day},_{latest_date.year}"