From 03af6e8686f2c607995b6c3d73d810d7d5f0b89a Mon Sep 17 00:00:00 2001 From: Abdel792 Date: Thu, 25 Jun 2026 10:21:43 +0200 Subject: [PATCH 1/3] Update addonTemplate to latest validated version and adjust Crowdin documentation sync Integrate the most recent validated version of addonTemplate. Update the documentation translation workflow to ignore Markdown files from Crowdin and process only XLIFF files. No changes to PO-based UI translation workflow. --- .github/scripts/checkTranslation.py | 5 +- .github/scripts/crowdinSync.ps1 | 128 ++- .github/scripts/langCodes.py | 60 -- .github/scripts/markdownTranslate.py | 881 ------------------ .../workflows/auto-update-translations.yaml | 14 - .github/workflows/build_addon.yml | 4 +- .github/workflows/crowdinL10n.yml | 4 +- .github/workflows/manual-release.yaml | 4 +- addon/doc/fr/readme.md | 4 +- addon/doc/tr/readme.md | 4 +- addon/doc/uk/readme.md | 196 ++++ addon/globalPlugins/askOpenRouter/__init__.py | 68 +- addon/globalPlugins/askOpenRouter/dialogs.py | 105 +-- .../globalPlugins/askOpenRouter/functions.py | 145 +-- addon/locale/uk/LC_MESSAGES/nvda.po | 202 ++++ pyproject.toml | 5 +- uv.lock | 191 ++-- 17 files changed, 769 insertions(+), 1251 deletions(-) delete mode 100644 .github/scripts/langCodes.py delete mode 100644 .github/scripts/markdownTranslate.py delete mode 100644 .github/workflows/auto-update-translations.yaml create mode 100644 addon/doc/uk/readme.md create mode 100644 addon/locale/uk/LC_MESSAGES/nvda.po diff --git a/.github/scripts/checkTranslation.py b/.github/scripts/checkTranslation.py index 346a9ef..9d03dc6 100644 --- a/.github/scripts/checkTranslation.py +++ b/.github/scripts/checkTranslation.py @@ -100,7 +100,7 @@ def getScoreFromApi(fileNameToSearch: str, langId: str) -> float: # Also handles underscore to dash conversion for Crowdin compatibility if langApi.lower().startswith(langId.lower().replace("_", "-")): progress = float(item["data"]["translationProgress"]) - return progress / 100 + return progress # Check pagination total. total = resp["pagination"]["totalCount"] @@ -139,9 +139,6 @@ def main(): # Default to poScore for .po and other localization files. print(f"poScore={score}") - # Exit with success (0) if there is at least 50% translated content. - sys.exit(0 if score > 0.5 else 1) - if __name__ == "__main__": main() diff --git a/.github/scripts/crowdinSync.ps1 b/.github/scripts/crowdinSync.ps1 index 9b99315..8e7bb92 100644 --- a/.github/scripts/crowdinSync.ps1 +++ b/.github/scripts/crowdinSync.ps1 @@ -23,15 +23,17 @@ if (Test-Path $mdFile) { try { Copy-Item "$addonId.xliff" $tempXliff -Force Write-Host "DEBUG: Updating XLIFF source based on readme.md..." - uv run .github/scripts/markdownTranslate.py updateXliff -m $mdFile -x $tempXliff -o $xliffFile - } finally { + ./l10nUtil.exe md2xliff $mdFile $xliffFile -o $tempXliff + } + finally { if (Test-Path $tempXliff) { Remove-Item $tempXliff -Force } } - } else { + } + else { Write-Host "DEBUG: XLIFF template not found. Creating new one from readme.md..." - uv run .github/scripts/markdownTranslate.py generateXliff -m $mdFile -o $xliffFile + ./l10nUtil.exe md2xliff $mdFile $xliffFile } } @@ -49,8 +51,10 @@ if (Test-Path $potFile) { if (Test-Path $xliffFile) { Write-Host "DEBUG: Uploading updated XLIFF source to Crowdin..." ./l10nUtil.exe uploadSourceFile "$xliffFile" -c $env:L10N_UTIL_CONFIG + git add "$xliffFile" git diff --staged --quiet + if ($LASTEXITCODE -ne 0) { git commit -m "Update $xliffFile for $addonId" } @@ -69,127 +73,155 @@ New-Item -ItemType Directory -Force -Path addon/doc | Out-Null $languageMappings = Get-Content -Raw ".github/scripts/languageMappings.json" | ConvertFrom-Json foreach ($dir in Get-ChildItem -Path "_addonL10n/$addonId" -Directory) { + $langCode = $dir.Name - if ($langCode -eq "en") { continue } + if ($langCode -eq "en") { + continue + } # --- Identify codes + $crowdinLang = $null - # Use the ."variable" syntax to correctly read the PSCustomObject from JSON if ($languageMappings.PSObject.Properties.Name -contains $langCode) { $crowdinLang = $languageMappings."$langCode" } - # Fallback: If no mapping is found, replace underscores with dashes for Crowdin compatibility if (-not $crowdinLang) { $crowdinLang = $langCode.Replace('_', '-') } - # The $langCode (folder name from Crowdin) represents the local repository language code. - # It matches the NVDA directory structure, so no extra mapping is needed. Write-Host "--- Processing Language: $langCode (Crowdin: $crowdinLang) ---" -ForegroundColor Cyan # Paths - $remoteMd = Join-Path $dir.FullName "$addonId.md" + $remoteXliff = Join-Path $dir.FullName "$addonId.xliff" $remotePo = Join-Path $dir.FullName "$addonId.po" + $localMdDir = "addon/doc/$langCode" $localMd = "$localMdDir/readme.md" + $localPoPath = "addon/locale/$langCode/LC_MESSAGES/nvda.po" # --- 3.1 PO FILE PROCESSING --- $poImported = $false + $scorePo = 0.0 + $threshold = $env:MIN_PERCENTAGE_TRANSLATED + if (Test-Path $remotePo) { - Write-Host "DEBUG: Checking Remote PO progress for $crowdinLang..." - uv run python .github/scripts/checkTranslation.py "$addonId.po" $crowdinLang - if ($LASTEXITCODE -eq 0) { - Write-Host "SUCCESS: Remote PO is valid. Importing to $localPoPath" + + Write-Host "DEBUG: Evaluating Remote PO score..." + + $res = uv run python .github/scripts/checkTranslation.py "$addonId.po" $crowdinLang + + $scorePo = [double]( + ($res | Select-String "poScore=").ToString().Split("=")[1] + ) + + Write-Host "DEBUG: PO Score -> $scorePo" + + if ($scorePo -ge $threshold) { + + Write-Host "SUCCESS: Remote PO is above threshold. Importing to $localPoPath" + New-Item -ItemType Directory -Force -Path (Split-Path $localPoPath) | Out-Null + Move-Item $remotePo $localPoPath -Force + $poImported = $true - } else { - Write-Host "WARNING: Remote PO progress is below threshold." + } + else { + + Write-Host "WARNING: Remote PO score is below threshold ($threshold)." } } if (-not $poImported -and (Test-Path $localPoPath)) { + Write-Host "ACTION: Uploading local legacy PO to Crowdin ($crowdinLang) as fallback." + ./l10nUtil.exe uploadTranslationFile $crowdinLang "$addonId.po" $localPoPath -c $env:L10N_UTIL_CONFIG } - # --- 3.2 DOCUMENTATION PROCESSING (MD & XLIFF) --- - $scoreMd = 0.0 - $scoreXliff = 0.0 + # --- 3.2 DOCUMENTATION PROCESSING (XLIFF ONLY) --- - if (Test-Path $remoteMd) { - Write-Host "DEBUG: Evaluating Remote Markdown score..." - $res = uv run python .github/scripts/checkTranslation.py "$addonId.md" $crowdinLang - $scoreMd = [double]($res | Select-String "mdScore=").ToString().Split("=")[1] - } else { - Write-Host "DEBUG: No remote Markdown file found for this language." - } + $scoreXliff = 0.0 if (Test-Path $remoteXliff) { + Write-Host "DEBUG: Evaluating Remote XLIFF score..." + $res = uv run python .github/scripts/checkTranslation.py "$addonId.xliff" $crowdinLang - $scoreXliff = [double]($res | Select-String "xliffScore=").ToString().Split("=")[1] - } else { + + $scoreXliff = [double]( + ($res | Select-String "xliffScore=").ToString().Split("=")[1] + ) + } + else { Write-Host "DEBUG: No remote XLIFF file found for this language." } - Write-Host "DEBUG: Comparison Scores -> MD: $scoreMd | XLIFF: $scoreXliff" + Write-Host "DEBUG: XLIFF Score -> $scoreXliff" - $threshold = 0.5 + $threshold = $env:MIN_PERCENTAGE_TRANSLATED $docImported = $false - if ($scoreXliff -gt $threshold -or $scoreMd -gt $threshold) { - if (!(Test-Path $localMdDir)) { New-Item -ItemType Directory -Force -Path $localMdDir | Out-Null } - - if ($scoreXliff -ge $scoreMd) { - Write-Host "SUCCESS: XLIFF is better or equal. Converting XLIFF to local MD ($langCode)..." - ./l10nUtil.exe xliff2md $remoteXliff $localMd - $docImported = $true - } else { - Write-Host "SUCCESS: Markdown is better. Importing Remote MD to local ($langCode)..." - Move-Item $remoteMd $localMd -Force - $docImported = $true + if ($scoreXliff -ge $threshold) { + + if (!(Test-Path $localMdDir)) { + New-Item -ItemType Directory -Force -Path $localMdDir | Out-Null } - } else { - Write-Host "WARNING: Both remote MD and XLIFF scores are below threshold ($threshold)." + + Write-Host "SUCCESS: Importing documentation from XLIFF ($langCode)..." + + ./l10nUtil.exe xliff2md $remoteXliff $localMd + + $docImported = $true } + else { - if (-not $docImported -and (Test-Path $localMd)) { - Write-Host "ACTION: Documentation quality too low. Uploading local MD to Crowdin ($crowdinLang) as fallback." - ./l10nUtil.exe uploadTranslationFile $crowdinLang "$addonId.md" $localMd -c $env:L10N_UTIL_CONFIG + Write-Host "WARNING: Remote XLIFF score is below threshold ($threshold)." } + + # No Markdown fallback upload anymore. + # XLIFF is now the single translation source in Crowdin. } # --- STEP 4: COMMIT UPDATED TRANSLATIONS --- git add addon/locale addon/doc + git diff --staged --quiet + if ($LASTEXITCODE -ne 0) { + git commit -m "Update translations for $addonId from Crowdin (Automatic Sync)" + Write-Host "SUCCESS: Translations committed." -} else { +} +else { + Write-Host "DEBUG: No changes in translations to commit." } # Push all generated commits after successful Crowdin synchronization + $pushOutput = git push 2>&1 -# Get the full repository name in "owner/repository" format (e.g., hkatic/clock) $repository = $env:GITHUB_REPOSITORY Write-Host $pushOutput if ($LASTEXITCODE -ne 0) { + Write-Host "ERROR: Failed to push commits to $repository." } elseif ($pushOutput -match "Everything up-to-date") { + Write-Host "INFO: No new commits needed to be pushed." } else { + Write-Host "SUCCESS: New commits successfully pushed to $repository." } diff --git a/.github/scripts/langCodes.py b/.github/scripts/langCodes.py deleted file mode 100644 index 044e5b5..0000000 --- a/.github/scripts/langCodes.py +++ /dev/null @@ -1,60 +0,0 @@ -import sys - -# Mapping between Crowdin language IDs (keys) and standard NVDA directory names (values). -# This dictionary acts as the symmetrical counterpart to 'languageMappings.json' implemented by @nvdaes. -# It ensures that translations exported from Crowdin are stored in the correct -# local paths (e.g., 'es-ES' from Crowdin goes into the 'es' folder). -CROWDIN_TO_NVDA = { - # Arabic variants - "ar-SA": "ar_SA", - - # Spanish variants - "es-ES": "es", - "es-CO": "es_CO", - - # Portuguese variants - "pt-BR": "pt_BR", - "pt-PT": "pt_PT", - - # Chinese variants - "zh-CN": "zh_CN", - "zh-HK": "zh_HK", - "zh-TW": "zh_TW", - - # Other specific mappings from the NVDA ecosystem - "af": "af_ZA", - "de-CH": "de_CH", - "nb": "nb_NO", - "nn-NO": "nn_NO", - "sr-CS": "sr" -} - -def get_nvda_code(crowdin_code): - """ - Returns the appropriate local directory name for a given Crowdin language ID. - - Args: - crowdin_code (str): The language identifier from Crowdin (e.g., 'pt-BR', 'fr'). - - Returns: - str: The corresponding NVDA locale folder name (e.g., 'pt_BR', 'fr'). - """ - # 1. Direct check in our verified map (Priority) - if crowdin_code in CROWDIN_TO_NVDA: - return CROWDIN_TO_NVDA[crowdin_code] - - # 2. Automated conversion for regional variants: Crowdin "xx-YY" -> NVDA "xx_YY" - # This handles regional codes not explicitly defined in the map. - if "-" in crowdin_code: - return crowdin_code.replace("-", "_") - - # 3. Default: Return as is. - # This covers base languages that don't use regional folders in NVDA - # (e.g., 'fr', 'tr', 'bg', 'fi', 'fa'). - return crowdin_code - -if __name__ == "__main__": - # Ensure a language code was provided as a command-line argument - if len(sys.argv) > 1: - # Standardize input and output the mapped code for PowerShell to capture - print(get_nvda_code(sys.argv[1])) \ No newline at end of file diff --git a/.github/scripts/markdownTranslate.py b/.github/scripts/markdownTranslate.py deleted file mode 100644 index 165e940..0000000 --- a/.github/scripts/markdownTranslate.py +++ /dev/null @@ -1,881 +0,0 @@ -# A part of NonVisual Desktop Access (NVDA) -# Copyright (C) 2024-2026 NV Access Limited. -# This file is covered by the GNU General Public License. -# See the file COPYING for more details. - -from collections.abc import Generator -from collections.abc import Iterable -import tempfile -import os -import contextlib -import lxml.etree -import argparse -import uuid -import re -from itertools import zip_longest -from xml.sax.saxutils import escape as xmlEscape -import difflib -from dataclasses import dataclass -import subprocess - - -def getGithubRepoURL() -> str | None: - """ - Get the GitHub repository URL from git remote origin. - return: The raw GitHub URL for the repository, or None if it cannot be determined. - """ - result = subprocess.run( - ["git", "remote", "get-url", "origin"], - capture_output=True, - text=True, - check=True, - ) - remote_url = result.stdout.strip() - # Convert SSH or HTTPS URL to raw GitHub URL format - if match := re.match(r"git@github\.com:(.+?)(?:\.git)?$", remote_url): - repo_path = match.group(1) - elif match := re.match(r"https://github\.com/(.+?)(?:\.git)?$", remote_url): - repo_path = match.group(1) - else: - raise ValueError(f"Cannot parse GitHub URL from git remote: {remote_url}") - return f"https://raw.githubusercontent.com/{repo_path}" - - -re_kcTitle = re.compile(r"^()$") -re_kcSettingsSection = re.compile(r"^()$") -# Comments that span a single line in their entirety -re_comment = re.compile(r"^$") -re_heading = re.compile(r"^(#+\s+)(.+?)((?:\s+\{#.+\})?)$") -re_bullet = re.compile(r"^(\s*\*\s+)(.+)$") -re_number = re.compile(r"^(\s*[0-9]+\.\s+)(.+)$") -re_hiddenHeaderRow = re.compile(r"^\|\s*\.\s*\{\.hideHeaderRow\}\s*(\|\s*\.\s*)*\|$") -re_postTableHeaderLine = re.compile(r"^(\|\s*-+\s*)+\|$") -re_tableRow = re.compile(r"^(\|)(.+)(\|)$") -re_translationID = re.compile(r"^(.*)\$\(ID:([0-9a-f-]+)\)(.*)$") -re_inlineMarkdownLintComment = re.compile(r"^(.*?)(?:\s*)(\s*)$") - - -def prettyPathString(path: str) -> str: - cwd = os.getcwd() - if os.path.normcase(os.path.splitdrive(path)[0]) != os.path.normcase( - os.path.splitdrive(cwd)[0], - ): - return path - return os.path.relpath(path, cwd) - - -@contextlib.contextmanager -def createAndDeleteTempFilePath_contextManager( - dir: str | None = None, - prefix: str | None = None, - suffix: str | None = None, -) -> Generator[str, None, None]: - """A context manager that creates a temporary file and deletes it when the context is exited""" - with tempfile.NamedTemporaryFile( - dir=dir, - prefix=prefix, - suffix=suffix, - delete=False, - ) as tempFile: - tempFilePath = tempFile.name - tempFile.close() - yield tempFilePath - os.remove(tempFilePath) - - -def getLastCommitID(filePath: str) -> str: - # Run the git log command to get the last commit ID for the given file - result = subprocess.run( - ["git", "log", "-n", "1", "--pretty=format:%H", "--", filePath], - capture_output=True, - text=True, - check=True, - ) - commitID = result.stdout.strip() - if not re.match(r"[0-9a-f]{40}", commitID): - raise ValueError(f"Invalid commit ID: '{commitID}' for file '{filePath}'") - return commitID - - -def getGitDir() -> str: - # Run the git rev-parse command to get the root of the git directory - result = subprocess.run( - ["git", "rev-parse", "--show-toplevel"], - capture_output=True, - text=True, - check=True, - ) - gitDir = result.stdout.strip() - if not os.path.isdir(gitDir): - raise ValueError(f"Invalid git directory: '{gitDir}'") - return gitDir - - -def getRawGithubURLForPath(filePath: str) -> str: - gitDirPath = getGitDir() - commitID = getLastCommitID(filePath) - relativePath = os.path.relpath(os.path.abspath(filePath), gitDirPath) - relativePath = relativePath.replace("\\", "/") - rawGithubRepoUrl = getGithubRepoURL() - return f"{rawGithubRepoUrl}/{commitID}/{relativePath}" - - -def preprocessMarkdownLines(mdLines: Iterable[str]) -> Iterable[str]: - """ - Preprocess markdown lines such as removing inline markdown lint comments.\ - :param mdLines: The markdown lines to preprocess - :returns: The preprocessed markdown lines - """ - for mdLine in mdLines: - # #18982: Remove markdown lint comments completely - not needed for intermediate markdown or final html. - mdLine = re_inlineMarkdownLintComment.sub(r"\1\2", mdLine) - yield mdLine - - -def skeletonizeLine(mdLine: str) -> str | None: - prefix = "" - suffix = "" - if ( - mdLine.isspace() - or mdLine.strip() == "[TOC]" - or re_hiddenHeaderRow.match(mdLine) - or re_postTableHeaderLine.match(mdLine) - ): - return None - elif m := re_heading.match(mdLine): - prefix, content, suffix = m.groups() - elif m := re_bullet.match(mdLine): - prefix, content = m.groups() - elif m := re_number.match(mdLine): - prefix, content = m.groups() - elif m := re_tableRow.match(mdLine): - prefix, content, suffix = m.groups() - elif m := re_kcTitle.match(mdLine): - prefix, content, suffix = m.groups() - elif m := re_kcSettingsSection.match(mdLine): - prefix, content, suffix = m.groups() - elif re_comment.match(mdLine): - return None - ID = str(uuid.uuid4()) - return f"{prefix}$(ID:{ID}){suffix}\n" - - -@dataclass -class Result_generateSkeleton: - numTotalLines: int = 0 - numTranslationPlaceholders: int = 0 - - -def generateSkeleton(mdPath: str, outputPath: str) -> Result_generateSkeleton: - print( - f"Generating skeleton file {prettyPathString(outputPath)} from {prettyPathString(mdPath)}...", - ) - res = Result_generateSkeleton() - with ( - open(mdPath, "r", encoding="utf8") as mdFile, - open(outputPath, "w", encoding="utf8", newline="") as outputFile, - ): - for mdLine in preprocessMarkdownLines(mdFile.readlines()): - res.numTotalLines += 1 - skelLine = skeletonizeLine(mdLine) - if skelLine: - res.numTranslationPlaceholders += 1 - else: - skelLine = mdLine - outputFile.write(skelLine) - print( - f"Generated skeleton file with {res.numTotalLines} total lines and {res.numTranslationPlaceholders} translation placeholders", - ) - return res - - -@dataclass -class Result_updateSkeleton: - numAddedLines: int = 0 - numAddedTranslationPlaceholders: int = 0 - numRemovedLines: int = 0 - numRemovedTranslationPlaceholders: int = 0 - numUnchangedLines: int = 0 - numUnchangedTranslationPlaceholders: int = 0 - - -def extractSkeleton(xliffPath: str, outputPath: str): - print( - f"Extracting skeleton from {prettyPathString(xliffPath)} to {prettyPathString(outputPath)}...", - ) - with contextlib.ExitStack() as stack: - outputFile = stack.enter_context( - open(outputPath, "w", encoding="utf8", newline=""), - ) - xliff = lxml.etree.parse(xliffPath) - xliffRoot = xliff.getroot() - namespace = {"xliff": "urn:oasis:names:tc:xliff:document:2.0"} - if xliffRoot.tag != "{urn:oasis:names:tc:xliff:document:2.0}xliff": - raise ValueError("Not an xliff file") - skeletonNode = xliffRoot.find( - "./xliff:file/xliff:skeleton", - namespaces=namespace, - ) - if skeletonNode is None: - raise ValueError("No skeleton found in xliff file") - skeletonContent = skeletonNode.text.strip() - outputFile.write(skeletonContent) - print(f"Extracted skeleton to {prettyPathString(outputPath)}") - - -def updateSkeleton( - origMdPath: str, - newMdPath: str, - origSkelPath: str, - outputPath: str, -) -> Result_updateSkeleton: - print( - f"Creating updated skeleton file {prettyPathString(outputPath)} from {prettyPathString(origSkelPath)} with changes from {prettyPathString(origMdPath)} to {prettyPathString(newMdPath)}...", - ) - res = Result_updateSkeleton() - with contextlib.ExitStack() as stack: - origMdFile = stack.enter_context(open(origMdPath, "r", encoding="utf8")) - newMdFile = stack.enter_context(open(newMdPath, "r", encoding="utf8")) - origSkelFile = stack.enter_context(open(origSkelPath, "r", encoding="utf8")) - outputFile = stack.enter_context( - open(outputPath, "w", encoding="utf8", newline=""), - ) - origMdLines = preprocessMarkdownLines(origMdFile.readlines()) - newMdLines = preprocessMarkdownLines(newMdFile.readlines()) - mdDiff = difflib.ndiff(list(origMdLines), list(newMdLines)) - origSkelLines = iter(origSkelFile.readlines()) - for mdDiffLine in mdDiff: - if mdDiffLine.startswith("?"): - continue - if mdDiffLine.startswith(" "): - res.numUnchangedLines += 1 - skelLine = next(origSkelLines) - if re_translationID.match(skelLine): - res.numUnchangedTranslationPlaceholders += 1 - outputFile.write(skelLine) - elif mdDiffLine.startswith("+"): - res.numAddedLines += 1 - skelLine = skeletonizeLine(mdDiffLine[2:]) - if skelLine: - res.numAddedTranslationPlaceholders += 1 - else: - skelLine = mdDiffLine[2:] - outputFile.write(skelLine) - elif mdDiffLine.startswith("-"): - res.numRemovedLines += 1 - origSkelLine = next(origSkelLines) - if re_translationID.match(origSkelLine): - res.numRemovedTranslationPlaceholders += 1 - else: - raise ValueError(f"Unexpected diff line: {mdDiffLine}") - print( - f"Updated skeleton file with {res.numAddedLines} added lines " - f"({res.numAddedTranslationPlaceholders} translation placeholders), " - f"{res.numRemovedLines} removed lines ({res.numRemovedTranslationPlaceholders} translation placeholders), " - f"and {res.numUnchangedLines} unchanged lines ({res.numUnchangedTranslationPlaceholders} translation placeholders)", - ) - return res - - -@dataclass -class Result_generateXliff: - numTranslatableStrings: int = 0 - - -def generateXliff( - mdPath: str, - outputPath: str, - skelPath: str | None = None, -) -> Result_generateXliff: - # If a skeleton file is not provided, first generate one - with contextlib.ExitStack() as stack: - if not skelPath: - skelPath = stack.enter_context( - createAndDeleteTempFilePath_contextManager( - dir=os.path.dirname(outputPath), - prefix=os.path.basename(mdPath), - suffix=".skel", - ), - ) - generateSkeleton(mdPath=mdPath, outputPath=skelPath) - with open(skelPath, "r", encoding="utf8") as skelFile: - skelContent = skelFile.read() - res = Result_generateXliff() - print( - f"Generating xliff file {prettyPathString(outputPath)} from {prettyPathString(mdPath)} and {prettyPathString(skelPath)}...", - ) - with contextlib.ExitStack() as stack: - mdFile = stack.enter_context(open(mdPath, "r", encoding="utf8")) - outputFile = stack.enter_context( - open(outputPath, "w", encoding="utf8", newline=""), - ) - fileID = os.path.basename(mdPath) - mdUri = getRawGithubURLForPath(mdPath) - print(f"Including Github raw URL: {mdUri}") - outputFile.write( - '\n' - f'\n' - f'\n', - ) - outputFile.write(f"\n{xmlEscape(skelContent)}\n\n") - res.numTranslatableStrings = 0 - for lineNo, (mdLine, skelLine) in enumerate( - zip_longest( - preprocessMarkdownLines(mdFile.readlines()), - skelContent.splitlines(keepends=True), - ), - start=1, - ): - mdLine = mdLine.rstrip() - skelLine = skelLine.rstrip() - if m := re_translationID.match(skelLine): - res.numTranslatableStrings += 1 - prefix, ID, suffix = m.groups() - if prefix and not mdLine.startswith(prefix): - raise ValueError( - f'Line {lineNo}: does not start with "{prefix}", {mdLine=}, {skelLine=}', - ) - if suffix and not mdLine.endswith(suffix): - raise ValueError( - f'Line {lineNo}: does not end with "{suffix}", {mdLine=}, {skelLine=}', - ) - source = mdLine[len(prefix) : len(mdLine) - len(suffix)] - outputFile.write( - f'\n\nline: {lineNo + 1}\n', - ) - if prefix: - outputFile.write( - f'prefix: {xmlEscape(prefix)}\n', - ) - if suffix: - outputFile.write( - f'suffix: {xmlEscape(suffix)}\n', - ) - outputFile.write( - "\n" - f"\n" - f"{xmlEscape(source)}\n" - "\n" - "\n", # fmt: skip - ) - else: - if mdLine != skelLine: - raise ValueError( - f"Line {lineNo}: {mdLine=} does not match {skelLine=}", - ) - outputFile.write("\n") - print( - f"Generated xliff file with {res.numTranslatableStrings} translatable strings", - ) - return res - - -@dataclass -class Result_translateXliff: - numTranslatedStrings: int = 0 - - -def updateXliff( - xliffPath: str, - mdPath: str, - outputPath: str, -): - # uses generateMarkdown, extractSkeleton, updateSkeleton, and generateXliff to generate an updated xliff file. - outputDir = os.path.dirname(outputPath) - print( - f"Generating updated xliff file {prettyPathString(outputPath)} from {prettyPathString(xliffPath)} and {prettyPathString(mdPath)}...", - ) - with contextlib.ExitStack() as stack: - origMdPath = stack.enter_context( - createAndDeleteTempFilePath_contextManager( - dir=outputDir, - prefix="generated_", - suffix=".md", - ), - ) - generateMarkdown(xliffPath=xliffPath, outputPath=origMdPath, translated=False) - origSkelPath = stack.enter_context( - createAndDeleteTempFilePath_contextManager( - dir=outputDir, - prefix="extracted_", - suffix=".skel", - ), - ) - extractSkeleton(xliffPath=xliffPath, outputPath=origSkelPath) - updatedSkelPath = stack.enter_context( - createAndDeleteTempFilePath_contextManager( - dir=outputDir, - prefix="updated_", - suffix=".skel", - ), - ) - updateSkeleton( - origMdPath=origMdPath, - newMdPath=mdPath, - origSkelPath=origSkelPath, - outputPath=updatedSkelPath, - ) - generateXliff( - mdPath=mdPath, - skelPath=updatedSkelPath, - outputPath=outputPath, - ) - print(f"Generated updated xliff file {prettyPathString(outputPath)}") - - -def translateXliff( - xliffPath: str, - lang: str, - pretranslatedMdPath: str, - outputPath: str, - allowBadAnchors: bool = False, -) -> Result_translateXliff: - print( - f"Creating {lang} translated xliff file {prettyPathString(outputPath)} from {prettyPathString(xliffPath)} using {prettyPathString(pretranslatedMdPath)}...", - ) - res = Result_translateXliff() - with contextlib.ExitStack() as stack: - pretranslatedMdFile = stack.enter_context( - open(pretranslatedMdPath, "r", encoding="utf8"), - ) - xliff = lxml.etree.parse(xliffPath) - xliffRoot = xliff.getroot() - namespace = {"xliff": "urn:oasis:names:tc:xliff:document:2.0"} - if xliffRoot.tag != "{urn:oasis:names:tc:xliff:document:2.0}xliff": - raise ValueError("Not an xliff file") - xliffRoot.set("trgLang", lang) - skeletonNode = xliffRoot.find( - "./xliff:file/xliff:skeleton", - namespaces=namespace, - ) - if skeletonNode is None: - raise ValueError("No skeleton found in xliff file") - skeletonContent = skeletonNode.text.strip() - for lineNo, (skelLine, pretranslatedLine) in enumerate( - zip_longest( - skeletonContent.splitlines(), - preprocessMarkdownLines(pretranslatedMdFile.readlines()), - ), - start=1, - ): - skelLine = skelLine.rstrip() - pretranslatedLine = pretranslatedLine.rstrip() - if m := re_translationID.match(skelLine): - prefix, ID, suffix = m.groups() - if prefix and not pretranslatedLine.startswith(prefix): - raise ValueError( - f'Line {lineNo} of translation does not start with "{prefix}", {pretranslatedLine=}, {skelLine=}', - ) - if suffix and not pretranslatedLine.endswith(suffix): - if allowBadAnchors and (m := re_heading.match(pretranslatedLine)): - print( - f"Warning: ignoring bad anchor in line {lineNo}: {pretranslatedLine}", - ) - suffix = m.group(3) - if suffix and not pretranslatedLine.endswith(suffix): - raise ValueError( - f'Line {lineNo} of translation: does not end with "{suffix}", {pretranslatedLine=}, {skelLine=}', - ) - translation = pretranslatedLine[len(prefix) : len(pretranslatedLine) - len(suffix)] - try: - unit = xliffRoot.find( - f'./xliff:file/xliff:unit[@id="{ID}"]', - namespaces=namespace, - ) - if unit is not None: - segment = unit.find("./xliff:segment", namespaces=namespace) - if segment is not None: - target = lxml.etree.Element("target") - target.text = translation - target.tail = "\n" - segment.append(target) - res.numTranslatedStrings += 1 - else: - raise ValueError(f"No segment found for unit {ID}") - else: - raise ValueError(f"Cannot locate Unit {ID} in xliff file") - except Exception as e: - e.add_note(f"Line {lineNo}: {pretranslatedLine=}, {skelLine=}") - raise - elif skelLine != pretranslatedLine: - raise ValueError( - f"Line {lineNo}: pretranslated line {pretranslatedLine!r}, does not match skeleton line {skelLine!r}", - ) - xliff.write(outputPath, encoding="utf8", xml_declaration=True) - print( - f"Translated xliff file with {res.numTranslatedStrings} translated strings", - ) - return res - - -@dataclass -class Result_generateMarkdown: - numTotalLines = 0 - numTranslatableStrings = 0 - numTranslatedStrings = 0 - numBadTranslationStrings = 0 - - -def generateMarkdown( - xliffPath: str, - outputPath: str, - translated: bool = True, -) -> Result_generateMarkdown: - print( - f"Generating markdown file {prettyPathString(outputPath)} from {prettyPathString(xliffPath)}...", - ) - res = Result_generateMarkdown() - with contextlib.ExitStack() as stack: - outputFile = stack.enter_context( - open(outputPath, "w", encoding="utf8", newline=""), - ) - xliff = lxml.etree.parse(xliffPath) - xliffRoot = xliff.getroot() - namespace = {"xliff": "urn:oasis:names:tc:xliff:document:2.0"} - if xliffRoot.tag != "{urn:oasis:names:tc:xliff:document:2.0}xliff": - raise ValueError("Not an xliff file") - skeletonNode = xliffRoot.find( - "./xliff:file/xliff:skeleton", - namespaces=namespace, - ) - if skeletonNode is None: - raise ValueError("No skeleton found in xliff file") - skeletonContent = skeletonNode.text.strip() - for lineNum, line in enumerate(skeletonContent.splitlines(keepends=True), 1): - res.numTotalLines += 1 - if m := re_translationID.match(line): - prefix, ID, suffix = m.groups() - res.numTranslatableStrings += 1 - unit = xliffRoot.find( - f'./xliff:file/xliff:unit[@id="{ID}"]', - namespaces=namespace, - ) - if unit is None: - raise ValueError(f"Cannot locate Unit {ID} in xliff file") - segment = unit.find("./xliff:segment", namespaces=namespace) - if segment is None: - raise ValueError(f"No segment found for unit {ID}") - source = segment.find("./xliff:source", namespaces=namespace) - if source is None: - raise ValueError(f"No source found for unit {ID}") - translation = "" - if translated: - target = segment.find("./xliff:target", namespaces=namespace) - if target is not None: - targetText = target.text - if targetText: - translation = targetText - # Crowdin treats empty targets () as a literal translation. - # Filter out such strings and count them as bad translations. - if translation in ( - "", - "<target/>", - "", - "<target></target>", - ): - res.numBadTranslationStrings += 1 - translation = "" - else: - res.numTranslatedStrings += 1 - # If we have no translation, use the source text - if not translation: - sourceText = source.text - if sourceText is None: - raise ValueError(f"No source text found for unit {ID}") - translation = sourceText - outputFile.write(f"{prefix}{translation}{suffix}\n") - else: - outputFile.write(line) - print( - f"Generated markdown file with {res.numTotalLines} total lines, {res.numTranslatableStrings} translatable strings, and {res.numTranslatedStrings} translated strings. Ignoring {res.numBadTranslationStrings} bad translated strings", - ) - return res - - -def ensureMarkdownFilesMatch(path1: str, path2: str, allowBadAnchors: bool = False): - print( - f"Ensuring files {prettyPathString(path1)} and {prettyPathString(path2)} match...", - ) - with contextlib.ExitStack() as stack: - file1 = stack.enter_context(open(path1, "r", encoding="utf8")) - file2 = stack.enter_context(open(path2, "r", encoding="utf8")) - for lineNo, (line1, line2) in enumerate( - zip_longest( - preprocessMarkdownLines(file1.readlines()), - preprocessMarkdownLines(file2.readlines()), - ), - start=1, - ): - line1 = line1.rstrip() - line2 = line2.rstrip() - if line1 != line2: - if ( - re_postTableHeaderLine.match(line1) - and re_postTableHeaderLine.match(line2) - and line1.count("|") == line2.count("|") - ): - print( - f"Warning: ignoring cell padding of post table header line at line {lineNo}: {line1}, {line2}", - ) - continue - if ( - re_hiddenHeaderRow.match(line1) - and re_hiddenHeaderRow.match(line2) - and line1.count("|") == line2.count("|") - ): - print( - f"Warning: ignoring cell padding of hidden header row at line {lineNo}: {line1}, {line2}", - ) - continue - if allowBadAnchors and (m1 := re_heading.match(line1)) and (m2 := re_heading.match(line2)): - print( - f"Warning: ignoring bad anchor in headings at line {lineNo}: {line1}, {line2}", - ) - line1 = m1.group(1) + m1.group(2) - line2 = m2.group(1) + m2.group(2) - if line1 != line2: - raise ValueError( - f"Files do not match at line {lineNo}: {line1=} {line2=}", - ) - print("Files match") - - -def markdownTranslateCommand(command: str, *args): - print(f"Running markdownTranslate command: {command} {' '.join(args)}") - subprocess.run(["python", __file__, command, *args], check=True) - - -def pretranslateAllPossibleLanguages(langsDir: str, mdBaseName: str): - # This function walks through all language directories in the given directory, skipping en (English) and translates the English xlif and skel file along with the lang's pretranslated md file - enXliffPath = os.path.join(langsDir, "en", f"{mdBaseName}.xliff") - if not os.path.exists(enXliffPath): - raise ValueError(f"English xliff file {enXliffPath} does not exist") - allLangs = set() - succeededLangs = set() - skippedLangs = set() - for langDir in os.listdir(langsDir): - if langDir == "en": - continue - langDirPath = os.path.join(langsDir, langDir) - if not os.path.isdir(langDirPath): - continue - langPretranslatedMdPath = os.path.join(langDirPath, f"{mdBaseName}.md") - if not os.path.exists(langPretranslatedMdPath): - continue - allLangs.add(langDir) - langXliffPath = os.path.join(langDirPath, f"{mdBaseName}.xliff") - if os.path.exists(langXliffPath): - print(f"Skipping {langDir} as the xliff file already exists") - skippedLangs.add(langDir) - continue - try: - translateXliff( - xliffPath=enXliffPath, - lang=langDir, - pretranslatedMdPath=langPretranslatedMdPath, - outputPath=langXliffPath, - allowBadAnchors=True, - ) - except Exception as e: - print(f"Failed to translate {langDir}: {e}") - continue - rebuiltLangMdPath = os.path.join(langDirPath, f"rebuilt_{mdBaseName}.md") - try: - generateMarkdown( - xliffPath=langXliffPath, - outputPath=rebuiltLangMdPath, - ) - except Exception as e: - print(f"Failed to rebuild {langDir} markdown: {e}") - os.remove(langXliffPath) - continue - try: - ensureMarkdownFilesMatch( - rebuiltLangMdPath, - langPretranslatedMdPath, - allowBadAnchors=True, - ) - except Exception as e: - print( - f"Rebuilt {langDir} markdown does not match pretranslated markdown: {e}", - ) - os.remove(langXliffPath) - continue - os.remove(rebuiltLangMdPath) - print(f"Successfully pretranslated {langDir}") - succeededLangs.add(langDir) - if len(skippedLangs) > 0: - print(f"Skipped {len(skippedLangs)} languages already pretranslated.") - print( - f"Pretranslated {len(succeededLangs)} out of {len(allLangs) - len(skippedLangs)} languages.", - ) - - -if __name__ == "__main__": - mainParser = argparse.ArgumentParser() - commandParser = mainParser.add_subparsers( - title="commands", - dest="command", - required=True, - ) - generateXliffParser = commandParser.add_parser("generateXliff") - generateXliffParser.add_argument( - "-m", - "--markdown", - dest="md", - type=str, - required=True, - help="The markdown file to generate the xliff file for", - ) - generateXliffParser.add_argument( - "-o", - "--output", - dest="output", - type=str, - required=True, - help="The file to output the xliff file to", - ) - updateXliffParser = commandParser.add_parser("updateXliff") - updateXliffParser.add_argument( - "-x", - "--xliff", - dest="xliff", - type=str, - required=True, - help="The original xliff file", - ) - updateXliffParser.add_argument( - "-m", - "--newMarkdown", - dest="md", - type=str, - required=True, - help="The new markdown file", - ) - updateXliffParser.add_argument( - "-o", - "--output", - dest="output", - type=str, - required=True, - help="The file to output the updated xliff to", - ) - translateXliffParser = commandParser.add_parser("translateXliff") - translateXliffParser.add_argument( - "-x", - "--xliff", - dest="xliff", - type=str, - required=True, - help="The xliff file to translate", - ) - translateXliffParser.add_argument( - "-l", - "--lang", - dest="lang", - type=str, - required=True, - help="The language to translate to", - ) - translateXliffParser.add_argument( - "-p", - "--pretranslatedMarkdown", - dest="pretranslatedMd", - type=str, - required=True, - help="The pretranslated markdown file to use", - ) - translateXliffParser.add_argument( - "-o", - "--output", - dest="output", - type=str, - required=True, - help="The file to output the translated xliff file to", - ) - generateMarkdownParser = commandParser.add_parser("generateMarkdown") - generateMarkdownParser.add_argument( - "-x", - "--xliff", - dest="xliff", - type=str, - required=True, - help="The xliff file to generate the markdown file for", - ) - generateMarkdownParser.add_argument( - "-o", - "--output", - dest="output", - type=str, - required=True, - help="The file to output the markdown file to", - ) - generateMarkdownParser.add_argument( - "-u", - "--untranslated", - dest="translated", - action="store_false", - help="Generate the markdown file with the untranslated strings", - ) - ensureMarkdownFilesMatchParser = commandParser.add_parser( - "ensureMarkdownFilesMatch", - ) - ensureMarkdownFilesMatchParser.add_argument( - dest="path1", - type=str, - help="The first markdown file", - ) - ensureMarkdownFilesMatchParser.add_argument( - dest="path2", - type=str, - help="The second markdown file", - ) - pretranslateLangsParser = commandParser.add_parser("pretranslateLangs") - pretranslateLangsParser.add_argument( - "-d", - "--langs-dir", - dest="langsDir", - type=str, - required=True, - help="The directory containing the language directories", - ) - pretranslateLangsParser.add_argument( - "-b", - "--md-base-name", - dest="mdBaseName", - type=str, - required=True, - help="The base name of the markdown files to pretranslate", - ) - args = mainParser.parse_args() - match args.command: - case "generateXliff": - generateXliff(mdPath=args.md, outputPath=args.output) - case "updateXliff": - updateXliff( - xliffPath=args.xliff, - mdPath=args.md, - outputPath=args.output, - ) - case "generateMarkdown": - generateMarkdown( - xliffPath=args.xliff, - outputPath=args.output, - translated=args.translated, - ) - case "translateXliff": - translateXliff( - xliffPath=args.xliff, - lang=args.lang, - pretranslatedMdPath=args.pretranslatedMd, - outputPath=args.output, - ) - case "pretranslateLangs": - pretranslateAllPossibleLanguages( - langsDir=args.langsDir, - mdBaseName=args.mdBaseName, - ) - case "ensureMarkdownFilesMatch": - ensureMarkdownFilesMatch(path1=args.path1, path2=args.path2) - case _: - raise ValueError(f"Unknown command: {args.command}") diff --git a/.github/workflows/auto-update-translations.yaml b/.github/workflows/auto-update-translations.yaml deleted file mode 100644 index 61d57c6..0000000 --- a/.github/workflows/auto-update-translations.yaml +++ /dev/null @@ -1,14 +0,0 @@ -name: Auto update translations - -on: - push: - branches: - - main - schedule: - # * is a special character in YAML so you have to quote this string - - cron: '0 0 * * 6' - -jobs: - auto_update_translations: - uses: abdel792/autoUpdateTranslations/.github/workflows/l10n-updates.yaml@8ac89c644395cf2aad6e03a4bb2be5c36526d12a - \ No newline at end of file diff --git a/.github/workflows/build_addon.yml b/.github/workflows/build_addon.yml index 59f9ee1..237a496 100644 --- a/.github/workflows/build_addon.yml +++ b/.github/workflows/build_addon.yml @@ -22,7 +22,7 @@ jobs: steps: - name: Checkout repo - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Install uv uses: astral-sh/setup-uv@v7 with: @@ -60,7 +60,7 @@ jobs: permissions: contents: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: download all artifacts uses: actions/download-artifact@v8 with: diff --git a/.github/workflows/crowdinL10n.yml b/.github/workflows/crowdinL10n.yml index 09b43f1..748ceb5 100644 --- a/.github/workflows/crowdinL10n.yml +++ b/.github/workflows/crowdinL10n.yml @@ -15,6 +15,7 @@ env: GH_TOKEN: ${{ github.token }} CROWDIN_PROJECT_ID: ${{ vars.CROWDIN_PROJECT_ID || 780748 }} L10N_UTIL_CONFIG: ${{ vars.L10N_UTIL_CONFIG || 'addon' }} + MIN_PERCENTAGE_TRANSLATED: ${{ vars.MIN_PERCENTAGE_TRANSLATED || 50 }} jobs: crowdinSync: @@ -31,7 +32,7 @@ jobs: Write-Host "Sleeping for $delaySeconds seconds..." Start-Sleep -Seconds $delaySeconds - name: Checkout add-on - uses: actions/checkout@v6 + uses: actions/checkout@v7 with: submodules: true - name: Set up Python @@ -56,6 +57,7 @@ jobs: run: | gh release download --repo nvaccess/nvdaL10n --pattern "l10nUtil.exe" - name: Download translations from Crowdin + if: ${{ env.crowdinAuthToken }} shell: pwsh env: ADDON_ID: ${{ steps.getAddonInfo.outputs.addonId }} diff --git a/.github/workflows/manual-release.yaml b/.github/workflows/manual-release.yaml index 2a896f6..1482278 100644 --- a/.github/workflows/manual-release.yaml +++ b/.github/workflows/manual-release.yaml @@ -150,8 +150,8 @@ jobs: with open("changelog.md", "w", encoding="utf-8") as f: f.write(text) - - shell: python + + shell: python - name: Check if there are any changes diff --git a/addon/doc/fr/readme.md b/addon/doc/fr/readme.md index b3673bd..f1a412b 100644 --- a/addon/doc/fr/readme.md +++ b/addon/doc/fr/readme.md @@ -46,7 +46,7 @@ Dans le panneau des paramètres de NVDA, juste après le champ "OpenRouter API K "Afficher la clé API" -Si elle est cochée, les caractères de la clé API deviennent visibles. +Si elle est cochée, les caractères de la clé API deviennent visibles. Par défaut, ils sont masqués pour des raisons de sécurité. ## Paramètres de sélection du modèle @@ -138,7 +138,7 @@ Si vous préférez n'afficher que la dernière réponse au lieu de l'historique ## Scripts non assignés -Les scripts suivants n'ont pas de raccourcis assignés. +Les scripts suivants n'ont pas de raccourcis assignés. Vous pouvez les définir dans : Préférences → Gestes de commandes → Ask OpenRouter diff --git a/addon/doc/tr/readme.md b/addon/doc/tr/readme.md index debcd90..09bf801 100644 --- a/addon/doc/tr/readme.md +++ b/addon/doc/tr/readme.md @@ -46,7 +46,7 @@ NVDA ayarları panelinde, "OpenRouter API Key" alanının hemen ardından şu et "Show API key" -İşaretlenirse API anahtarının karakterleri görünür hale gelir. +İşaretlenirse API anahtarının karakterleri görünür hale gelir. Varsayılan olarak güvenlik nedeniyle gizlenirler. ## Model Seçim Ayarları @@ -138,7 +138,7 @@ Konuşma geçmişinin tamamı yerine yalnızca en son yanıtı görüntülemeyi ## Atanmamış Komut Dosyaları -Aşağıdaki komutlara hareket atanmamıştır. +Aşağıdaki komutlara hareket atanmamıştır. Bunları şu yoldan tanımlayabilirsiniz: Tercihler → Girdi Hareketleri → Ask OpenRouter diff --git a/addon/doc/uk/readme.md b/addon/doc/uk/readme.md new file mode 100644 index 0000000..4622c51 --- /dev/null +++ b/addon/doc/uk/readme.md @@ -0,0 +1,196 @@ +# Запитати OpenRouter + +* Автор: Abdel. + +Цей додаток для NVDA дозволяє взаємодіяти з моделями штучного інтелекту, що надаються платформою OpenRouter, безпосередньо з екранного зчитувача. + +Цей додаток підтримує обидві функції: +* Автоматичний випадковий вибір безкоштовних моделей +* Ручний вибір будь-якої доступної моделі (включно з платними) + +## Основні функції + +* Швидкий доступ: відкривайте інтерфейс чату в будь-який час за допомогою глобальної комбінації клавіш. +* Керування розмовами: розпочинайте нову розмову або продовжуйте попередню. +* Розумна зміна безкоштовних моделей: автоматичний вибір випадкової безкоштовної моделі для оптимізації щоденних квот використання. +* Ручний вибір моделі: обирайте конкретну модель (включаючи платні) у панелі налаштувань. +* Зручний перегляд результатів: переглядайте відповіді у чіткому, зручному для навігації діалозі з можливістю показу повної історії. + +## Конфігурація: отримання та встановлення ключа API + +Щоб використовувати цей додаток, ви повинні мати ключ API від OpenRouter. + +Навіть при використанні безкоштовних моделей ключ потрібен для ідентифікації ваших запитів. + +### 1. Як отримати ключ API + +1. Перейдіть на сайт [OpenRouter.ai](https://openrouter.ai/). +2. Створіть обліковий запис, натиснувши «Зареєструватися» (ви можете увійти за допомогою облікового запису GitHub, Google або MetaMask, а також за допомогою своєї електронної адреси). +3. Після входу перейдіть до розділу «Ключі» на вашій панелі керування або перейдіть безпосередньо за посиланням: https://openrouter.ai/keys +4. Натисніть кнопку «Створити ключ». +5. Дайте своєму ключу назву (наприклад: «Мій ключ API OpenRouter») і натисніть «Створити». +6. Важливо: Ваш ключ буде показано лише один раз. Негайно скопіюйте його та збережіть у безпечному місці. + +### 2. Налаштування ключа в NVDA + +1. Відкрийте меню NVDA (клавіші NVDA + N). +2. Перейдіть до розділу «Параметри», а потім — «Налаштування». +3. У списку категорій виберіть «Запитати OpenRouter». +4. Вставте свій ключ API у поле «Ключ API OpenRouter». +5. Натисніть «OK», щоб зберегти. + +#### Показати ключ API + +На панелі налаштувань NVDA, одразу після поля «Ключ API OpenRouter», розташований прапорець із назвою: + +«Показати ключ API» + +Якщо позначити прапорець, символи API-ключа стануть видимими. +Початково вони приховані з міркувань безпеки. + +## Параметри вибору моделі + +У розділі налаштувань «Запитати OpenRouter» ви знайдете новий параметр: + +### "Використовувати всі моделі, включаючи платні" + +Цей параметр керує способом вибору моделей. + +### Коли параметр НЕ ПОЗНАЧЕНО (поведінка початкова) + +* Додаток автоматично вибирає випадкову безкоштовну модель для кожного нового діалогу. +* Він по черзі використовує доступні безкоштовні моделі. +* Це допомагає рівномірно розподілити навантаження та уникнути обмежень за швидкістю. + +### Коли параметр ПОЗНАЧЕНО + +Якщо цей параметр позначено, після прапорця автоматично з’являється список доступних моделей. + +* Список відсортовано за зростанням ціни за вхідний токен (вартість одного токена запиту), від найнижчої до найвищої. +* Показуються лише неприховані моделі з дійсними постачальниками. + +### Що можна робити, коли цей параметр увімкнено? + +* Оберіть будь-яку доступну модель. +* Використовуйте платні моделі (якщо у вас достатньо кредитів OpenRouter). +* Оберіть модель, яка найкраще відповідає вашим потребам. +* Продовжуйте використовувати одну й ту саму обрану модель для своїх розмов (без автоматичної зміни). + +### Що таке підказка? + +Токен підказки — це невеликий фрагмент тексту, що надсилається до моделі (ваше запитання або вхідні дані). + +Зазвичай з моделей окремо стягується плата за: +* Токени вхідних даних (підказка) +* Токени вихідних даних (завершення) + +## Як використовувати + +### Відкриття діалогу чату + +Натисніть: + +Ctrl + Alt + A + +Ви можете змінити цей жест у: +Меню NVDA → Параметри → Жести вводу → Запитати OpenRouter + +### Основний інтерфейс + +Діалог містить три кнопки: + +1. Новий чат — розпочинає нову розмову. +2. Продовжити чат — продовжує попередню розмову (історія зберігається). +3. Закрити — закриває діалогове вікно (також можна натиснути клавішу Escape). + +### Введення підказки + +Вибравши «Новий чат» або «Продовжити чат»: + +* З'явиться багаторядкове текстове поле. +* Натискання клавіші Enter вставляє новий рядок. +* Щоб надіслати повідомлення: + - Натисніть клавішу Tab, щоб перейти до кнопки «OK». + - Натисніть клавішу Enter. + +### Читання відповіді + +Після обробки з'являється вікно з результатами, яке містить: + +* «Ви сказали:», а потім — ваше повідомлення. +* «Відповідь моделі:», а потім — відповідь. +* Кнопка «Копіювати» для копіювання відповіді. + +Якщо ввімкнено показ повної історії, кожен обмін чітко розділено заголовками, що полегшує навігацію за допомогою клавіш швидкої навігації NVDA. + +## Параметри показу + +Якщо ви бажаєте показувати лише останню відповідь замість повної історії розмов: + +1. Відкрийте меню NVDA (NVDA + N). +2. Перейдіть до Параметри → Налаштування. +3. Виберіть Запитати OpenRouter. +4. Зніміть прапорець: + «Показувати повну історію чату для безперервних обговорень» +5. Натисніть OK. + +## Непризначені сценарії + +Нижченаведені сценарії не мають жестів. +Ви можете призначити їх у: + +Параметри → Жести вводу → Запитати OpenRouter + +Доступні сценарії: + +* Відкрити панель налаштувань додатка +* Почати новий чат безпосередньо +* Продовжити наявний чат безпосередньо + +## Безкоштовні моделі, платні моделі та квоти + +### Безкоштовне використання моделі + +Коли не позначено прапорець «Використовувати всі моделі, включаючи платні»: + +* Використовуються лише моделі, позначені на OpenRouter як безкоштовні. +* Безкоштовні моделі мають: + - Обмежені добові квоти + - Спільні обмеження швидкості + - Можливу тимчасову недоступність + +Додаток автоматично змінює безкоштовні моделі для підвищення доступності. + +### Платне використання моделі + +Коли позначено прапорець «Використовувати всі моделі, включаючи платні»: + +* Додаток використовує саме ту модель, яку ви обрали. +* Це можуть бути й платні моделі. +* У вас має бути достатньо кредитів OpenRouter. +* Можуть застосовуватися обмеження постачальника щодо швидкості. + +Такі помилки, як: +* 402 (недостатньо кредитів) +* 429 (обмеження частоти запитів) +* 404 (модель заборонена налаштуваннями конфіденційності) + +показуються безпосередньо, щоб повідомити вас про проблему. + +## Нагадування про налаштування конфіденційності + +Якщо ви використовуєте безкоштовні моделі та отримуєте повідомлення про помилку: + +> "Не знайдено кінцевих точок, які відповідають вашій політиці даних" + +Можливо, вам знадобиться змінити налаштування конфіденційності OpenRouter: + +https://openrouter.ai/settings/privacy + +Переконайтеся, що публічні/безкоштовні кінцеві точки моделі дозволені. + +## Сумісність ## + +* Цей додаток сумісний із версіями NVDA починаючи з 2025.1 і вище. + +* Переклад: Георгій Галас. \ No newline at end of file diff --git a/addon/globalPlugins/askOpenRouter/__init__.py b/addon/globalPlugins/askOpenRouter/__init__.py index b5d147a..20162fb 100644 --- a/addon/globalPlugins/askOpenRouter/__init__.py +++ b/addon/globalPlugins/askOpenRouter/__init__.py @@ -1,3 +1,4 @@ +# pyright: reportUnusedCallResult=false # globalPlugins/askOpenRouter/__init__.py # Copyright(C) 2026-2028 Abdel @@ -5,13 +6,15 @@ # This file is covered by the GNU General Public License. # See the file COPYING for more details. +from collections.abc import Callable import scriptHandler import config import wx import addonHandler import globalPluginHandler import gui -from typing import Callable +import typing +from typing import Any, cast from .dialogs import addonSummary, OpenRouterSettingsPanel, ChatDialog from gui.settingsDialogs import NVDASettingsDialog from .functions import disableInSecureMode @@ -25,63 +28,82 @@ class GlobalPlugin(globalPluginHandler.GlobalPlugin): scriptCategory = addonSummary - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) - if "askOpenRouter" not in config.conf.spec: - config.conf.spec["askOpenRouter"] = { + # Use cast(Any) to prevent reportUnknownMemberType on config.conf.spec + conf_any = cast(Any, config.conf) + if "askOpenRouter" not in conf_any.spec: + conf_any.spec["askOpenRouter"] = { "apiKey": "string(default='')", "fullHistory": "boolean(default=True)", "useAllModels": "boolean(default=False)", - "selectedModel": "string(default='')" + "selectedModel": "string(default='')", } gui.settingsDialogs.NVDASettingsDialog.categoryClasses.append( - OpenRouterSettingsPanel + OpenRouterSettingsPanel, ) - def terminate(self): + @typing.override + def terminate(self) -> None: if OpenRouterSettingsPanel in gui.settingsDialogs.NVDASettingsDialog.categoryClasses: gui.settingsDialogs.NVDASettingsDialog.categoryClasses.remove( - OpenRouterSettingsPanel + OpenRouterSettingsPanel, ) - def onChatDialog(self, evt): - gui.mainFrame.prePopup() - dialog = ChatDialog(gui.mainFrame) + def onChatDialog(self, evt: Any) -> None: + # Guard against Optional (MainFrame | None) type on gui.mainFrame + mainFrame = gui.mainFrame + if mainFrame is None: + return + + mainFrame.prePopup() + dialog = ChatDialog(mainFrame) dialog.Show() - gui.mainFrame.postPopup() + mainFrame.postPopup() @scriptHandler.script( # Translators: Description of the script which opens the interaction dialog box with OpenRouter. description=_("Opens a dialog allowing the user to ask questions to OpenRouter"), gesture="kb:control+alt+a", ) - def script_openRouterDialog(self, gesture): - wx.CallAfter(self.onChatDialog, gui.mainFrame) + def script_openRouterDialog(self, gesture: Any) -> None: + wx.CallAfter(self.onChatDialog, None) @scriptHandler.script( # Translators: Description of the script which allows to create a new chat. description=_("Opens the prompt to start a new OpenRouter chat."), ) - def script_newChat(self, gesture): - dialog = ChatDialog(gui.mainFrame) - dialog.onNew(None) + def script_newChat(self, gesture: Any) -> None: + mainFrame = gui.mainFrame + if mainFrame is None: + return + dialog = ChatDialog(mainFrame) + dialog.onNew(cast(wx.CommandEvent, None)) @scriptHandler.script( # Translators: Description of the script which allows to continue an existing chat. description=_("Opens the prompt to continue an existing OpenRouter chat."), ) - def script_continueChat(self, gesture): - dialog = ChatDialog(gui.mainFrame) - dialog.onContinue(None) + def script_continueChat(self, gesture: Any) -> None: + mainFrame = gui.mainFrame + if mainFrame is None: + return + dialog = ChatDialog(mainFrame) + dialog.onContinue(cast(wx.CommandEvent, None)) @scriptHandler.script( # Translators: Description of the script which allows to show OpenRouter settings panel.. description=_("Opens the add-on settings panel."), ) - def script_showOpenRouterSettingsPanel(self, gesture): - gui.mainFrame.popupSettingsDialog( + def script_showOpenRouterSettingsPanel(self, gesture: Any) -> None: + mainFrame = gui.mainFrame + if mainFrame is None: + return + + # Use cast(Any) to bypass the partially unknown type of popupSettingsDialog + cast(Any, mainFrame).popupSettingsDialog( NVDASettingsDialog, - OpenRouterSettingsPanel + OpenRouterSettingsPanel, ) diff --git a/addon/globalPlugins/askOpenRouter/dialogs.py b/addon/globalPlugins/askOpenRouter/dialogs.py index 621ad41..dbc23a5 100644 --- a/addon/globalPlugins/askOpenRouter/dialogs.py +++ b/addon/globalPlugins/askOpenRouter/dialogs.py @@ -1,13 +1,16 @@ +# pyright: reportUnusedCallResult=false # globalPlugins/askOpenRouter/dialogs.py # Copyright(C) 2026-2028 Abdel # Released under GPL 2 +from collections.abc import Callable +import typing +from typing import Any, cast import wx import addonHandler import config import gui -from typing import Callable, List, Dict, Optional, cast from gui.settingsDialogs import SettingsPanel from .functions import askOpenRouter, inputBox, getAvailableModels @@ -15,7 +18,7 @@ addonHandler.initTranslation() _: Callable[[str], str] -addonSummary: str = addonHandler.getCodeAddon().manifest["summary"] +addonSummary: str = cast(str, cast(Any, addonHandler).getCodeAddon().manifest["summary"]) # Chat Dialog @@ -29,9 +32,9 @@ class ChatDialog(wx.Dialog): - Close the dialog """ - _instance: Optional["ChatDialog"] = None + _instance: "ChatDialog | None" = None - def __new__(cls, *args, **kwargs) -> "ChatDialog": + def __new__(cls, *args: Any, **kwargs: Any) -> "ChatDialog": if not ChatDialog._instance: return super().__new__(cls, *args, **kwargs) return ChatDialog._instance @@ -51,49 +54,48 @@ def __init__(self, parent: wx.Window) -> None: super().__init__( parent, # Translators: Title of the dialog box. - title=_("Chat Manager") + title=_("Chat Manager"), ) mainSizer: wx.BoxSizer = wx.BoxSizer(wx.VERTICAL) sHelper: gui.guiHelper.BoxSizerHelper = gui.guiHelper.BoxSizerHelper( self, - wx.VERTICAL + wx.VERTICAL, ) - buttonGroup: gui.guiHelper.ButtonHelper = \ - gui.guiHelper.ButtonHelper(wx.HORIZONTAL) + buttonGroup: gui.guiHelper.ButtonHelper = gui.guiHelper.ButtonHelper(wx.HORIZONTAL) self.newButton: wx.Button = buttonGroup.addButton( self, # Translators: Prompt label to create a new chat. - label=_("C&reate a New Chat") + label=_("C&reate a New Chat"), ) self.continueButton: wx.Button = buttonGroup.addButton( self, # Translators: Prompt label to continue an existing chat. - label=_("Co&ntinue a Chat") + label=_("Co&ntinue a Chat"), ) self.closeButton: wx.Button = buttonGroup.addButton( self, # Translators: Label of the closing button. - label=_("&Close") + label=_("&Close"), ) self.newButton.SetDefault() self.SetEscapeId(self.closeButton.GetId()) - self.newButton.Bind(wx.EVT_BUTTON, self.onNew) - self.continueButton.Bind(wx.EVT_BUTTON, self.onContinue) - self.Bind(wx.EVT_BUTTON, self.onClose, self.closeButton) + self.newButton.Bind(wx.EVT_BUTTON, self.onNew) # type: ignore + self.continueButton.Bind(wx.EVT_BUTTON, self.onContinue) # type: ignore + self.Bind(wx.EVT_BUTTON, self.onClose, self.closeButton) # type: ignore sHelper.addItem(buttonGroup) - mainSizer.Add( + mainSizer.Add( # type: ignore sHelper.sizer, border=gui.guiHelper.BORDER_FOR_DIALOGS, - flag=wx.ALL | wx.EXPAND + flag=wx.ALL | wx.EXPAND, ) self.SetSizerAndFit(mainSizer) @@ -116,7 +118,7 @@ def onNew(self, evt: wx.CommandEvent) -> None: inputBox( # Translators: Title of the dialog box to start a new chat. _("New Chat"), - askOpenRouter + askOpenRouter, ) def onContinue(self, evt: wx.CommandEvent) -> None: @@ -127,11 +129,10 @@ def onContinue(self, evt: wx.CommandEvent) -> None: # Translators: Title of the dialog box to continue an existing chat. _("Continue Chat"), askOpenRouter, - new=False + new=False, ) - # Settings Panel class OpenRouterSettingsPanel(SettingsPanel): """ @@ -147,15 +148,15 @@ class OpenRouterSettingsPanel(SettingsPanel): title: str = addonSummary - def makeSettings(self, settingsSizer: wx.Sizer) -> None: + @typing.override + def makeSettings(self, sizer: wx.Sizer) -> None: """ Build the settings UI components. Args: - settingsSizer (wx.Sizer): Parent sizer provided by NVDA. + sizer (wx.Sizer): Parent sizer provided by NVDA. """ - self.sHelper: gui.guiHelper.BoxSizerHelper = \ - gui.guiHelper.BoxSizerHelper(self, sizer=settingsSizer) + self.sHelper: gui.guiHelper.BoxSizerHelper = gui.guiHelper.BoxSizerHelper(self, sizer=sizer) # ========================= # API KEY @@ -164,18 +165,21 @@ def makeSettings(self, settingsSizer: wx.Sizer) -> None: self.apiKeyLabel: wx.StaticText = wx.StaticText( self, # Translators: Label of the field that must contain the OpenRouter API key. - label=_("OpenRouter API Key:") + label=_("OpenRouter API Key:"), ) self.sHelper.addItem(self.apiKeyLabel) self.apiKeyHidden: wx.TextCtrl = wx.TextCtrl( self, - style=wx.TE_PASSWORD + style=wx.TE_PASSWORD, ) self.sHelper.addItem(self.apiKeyHidden, flag=wx.EXPAND) + # OpenRouter configuration dictionary + conf_dict = cast(dict[str, Any], config.conf["askOpenRouter"]) + self.apiKeyHidden.SetValue( - config.conf["askOpenRouter"]["apiKey"] + cast(str, conf_dict["apiKey"]), ) self.apiKeyVisible: wx.TextCtrl = wx.TextCtrl(self) @@ -185,13 +189,13 @@ def makeSettings(self, settingsSizer: wx.Sizer) -> None: self.showApiKeyCheckBox: wx.CheckBox = wx.CheckBox( self, # Translators: Label of the checkbox to display the OpenRouter API key. - label=_("Show API key") + label=_("Show API key"), ) self.sHelper.addItem(self.showApiKeyCheckBox) - self.showApiKeyCheckBox.Bind( + self.showApiKeyCheckBox.Bind( # type: ignore wx.EVT_CHECKBOX, - self.onToggleApiVisibility + self.onToggleApiVisibility, ) # ========================= @@ -201,12 +205,12 @@ def makeSettings(self, settingsSizer: wx.Sizer) -> None: self.fullHistoryCheckBox: wx.CheckBox = wx.CheckBox( self, # Translators: Label of the checkbox to display chat history. - label=_("Display the full chat history for continuous discussions") + label=_("Display the full chat history for continuous discussions"), ) self.sHelper.addItem(self.fullHistoryCheckBox) self.fullHistoryCheckBox.SetValue( - config.conf["askOpenRouter"]["fullHistory"] + cast(bool, conf_dict["fullHistory"]), ) # ========================= @@ -215,19 +219,18 @@ def makeSettings(self, settingsSizer: wx.Sizer) -> None: self.useAllModelsCheckBox: wx.CheckBox = wx.CheckBox( self, - # Translators: # Translators: Label of the checkbox to show the list of models, including paid ones. - label=_("Use all models, including paid ones.") + label=_("Use all models, including paid ones."), ) self.sHelper.addItem(self.useAllModelsCheckBox) self.useAllModelsCheckBox.SetValue( - config.conf["askOpenRouter"].get("useAllModels", False) + cast(bool, conf_dict.get("useAllModels", False)), ) - self.useAllModelsCheckBox.Bind( + self.useAllModelsCheckBox.Bind( # type: ignore wx.EVT_CHECKBOX, - self.onToggleModelsList + self.onToggleModelsList, ) # ========================= @@ -238,7 +241,7 @@ def makeSettings(self, settingsSizer: wx.Sizer) -> None: self.sHelper.addItem(self.modelsList, flag=wx.EXPAND) self.modelsList.Hide() - self.modelsData: List[Dict[str, object]] = [] + self.modelsData: list[dict[str, object]] = [] wx.CallAfter(self.onToggleModelsList, None) @@ -259,7 +262,7 @@ def onToggleApiVisibility(self, evt: wx.CommandEvent) -> None: self.Layout() - def onToggleModelsList(self, evt: Optional[wx.CommandEvent]) -> None: + def onToggleModelsList(self, evt: wx.CommandEvent | None) -> None: """ Show or hide the models list depending on checkbox state. """ @@ -289,8 +292,7 @@ def _loadModelsIfNeeded(self) -> None: return try: - models: List[Dict[str, object]] = \ - getAvailableModels(apiKey) + models: list[dict[str, object]] = getAvailableModels(apiKey) except Exception: return @@ -298,7 +300,7 @@ def _loadModelsIfNeeded(self) -> None: self.modelsData = models - displayNames: List[str] = [] + displayNames: list[str] = [] for m in models: price: float = cast(float, m["promptPricing"]) @@ -307,10 +309,8 @@ def _loadModelsIfNeeded(self) -> None: self.modelsList.Set(displayNames) - savedModel: str = config.conf["askOpenRouter"].get( - "selectedModel", - "" - ) + conf_dict = cast(dict[str, Any], config.conf["askOpenRouter"]) + savedModel: str = cast(str, conf_dict.get("selectedModel", "")) for index, m in enumerate(models): if m["id"] == savedModel: @@ -328,21 +328,18 @@ def getApiKeyValue(self) -> str: return self.apiKeyVisible.GetValue() return self.apiKeyHidden.GetValue() + @typing.override def onSave(self) -> None: """ Save settings into NVDA configuration. """ - config.conf["askOpenRouter"]["apiKey"] = \ - self.getApiKeyValue().strip() - - config.conf["askOpenRouter"]["fullHistory"] = \ - self.fullHistoryCheckBox.GetValue() + conf_dict = cast(dict[str, Any], config.conf["askOpenRouter"]) - config.conf["askOpenRouter"]["useAllModels"] = \ - self.useAllModelsCheckBox.GetValue() + conf_dict["apiKey"] = self.getApiKeyValue().strip() + conf_dict["fullHistory"] = self.fullHistoryCheckBox.GetValue() + conf_dict["useAllModels"] = self.useAllModelsCheckBox.GetValue() index: int = self.modelsList.GetSelection() if index != wx.NOT_FOUND and self.modelsData: - config.conf["askOpenRouter"]["selectedModel"] = \ - self.modelsData[index]["id"] \ No newline at end of file + conf_dict["selectedModel"] = self.modelsData[index]["id"] diff --git a/addon/globalPlugins/askOpenRouter/functions.py b/addon/globalPlugins/askOpenRouter/functions.py index ef05d81..dceb68d 100644 --- a/addon/globalPlugins/askOpenRouter/functions.py +++ b/addon/globalPlugins/askOpenRouter/functions.py @@ -1,3 +1,4 @@ +# pyright: reportUnusedCallResult=false # globalPlugins/askOpenRouter/functions.py # Copyright(C) 2026-2028 Abdel @@ -5,6 +6,7 @@ # This file is covered by the GNU General Public License. # See the file COPYING for more details. +from collections.abc import Callable import wx import globalPluginHandler import globalVars @@ -20,22 +22,23 @@ import urllib.request import urllib.error import time -from typing import List, Dict, Callable, Optional, Any +from typing import Any, cast + addonHandler.initTranslation() _: Callable[[str], str] # Temporary in-memory blacklist for unavailable models -_unavailableModels: Dict[str, float] = {} +_unavailableModels: dict[str, float] = {} # Cooldowns (seconds) -_RATE_LIMIT_COOLDOWN: int = 300 # 429 -_POLICY_COOLDOWN: int = 180 # 404 -_PAYMENT_COOLDOWN: int = 1800 # 402 +_RATE_LIMIT_COOLDOWN: int = 300 # 429 +_POLICY_COOLDOWN: int = 180 # 404 +_PAYMENT_COOLDOWN: int = 1800 # 402 -def disableInSecureMode(decoratedCls): +def disableInSecureMode(decoratedCls: Any) -> Any: """ Decorator created by Luke Davis, member of the nvda-addons mailing list. """ @@ -75,12 +78,12 @@ def loadModel(filename: str) -> str: return "" -def saveHistory(history: List[Dict[str, str]], filename: str) -> None: +def saveHistory(history: list[dict[str, str]], filename: str) -> None: """ Serialize and save conversation history to disk. Args: - history (List[Dict[str, str]]): Conversation messages. + history (list[dict[str, str]]): Conversation messages. filename (str): Destination file path. Returns: @@ -90,7 +93,7 @@ def saveHistory(history: List[Dict[str, str]], filename: str) -> None: pickle.dump(history, f) -def loadHistory(filename: str) -> List[Dict[str, str]]: +def loadHistory(filename: str) -> list[dict[str, str]]: """ Load serialized conversation history from disk. @@ -98,7 +101,7 @@ def loadHistory(filename: str) -> List[Dict[str, str]]: filename (str): Path to history file. Returns: - List[Dict[str, str]]: Loaded conversation history, or an empty list. + list[dict[str, str]]: Loaded conversation history, or an empty list. """ if os.path.exists(filename): with open(filename, "rb") as f: @@ -115,10 +118,7 @@ def _cleanupUnavailableModels() -> None: """ currentTime: float = time.time() - expired = [ - model for model, expiry in _unavailableModels.items() - if currentTime > expiry - ] + expired = [model for model, expiry in _unavailableModels.items() if currentTime > expiry] for model in expired: del _unavailableModels[model] @@ -173,7 +173,7 @@ def getRandomFreeModel(apiKey: str) -> str: modelsURL: str = "https://openrouter.ai/api/v1/models" - headers: Dict[str, str] = { + headers: dict[str, str] = { "Authorization": f"Bearer {apiKey}", "User-Agent": "Python-urllib", } @@ -185,7 +185,7 @@ def getRandomFreeModel(apiKey: str) -> str: models = data["data"] - candidates: List[str] = [ + candidates: list[str] = [ m["id"] for m in models if float(m.get("pricing", {}).get("prompt", 1)) == 0 @@ -203,7 +203,7 @@ def getRandomFreeModel(apiKey: str) -> str: return random.choice(candidates) -def getAvailableModels(apiKey: str) -> List[Dict[str, object]]: +def getAvailableModels(apiKey: str) -> list[dict[str, object]]: """ Retrieve the full list of available models for the current user. @@ -226,7 +226,7 @@ def getAvailableModels(apiKey: str) -> List[Dict[str, object]]: apiKey (str): OpenRouter API key. Returns: - List[Dict[str, object]]: List of available model metadata. + list[dict[str, object]]: List of available model metadata. Raises: urllib.error.URLError: If network request fails. @@ -234,7 +234,7 @@ def getAvailableModels(apiKey: str) -> List[Dict[str, object]]: """ modelsURL: str = "https://openrouter.ai/api/v1/models" - headers: Dict[str, str] = { + headers: dict[str, str] = { "Authorization": f"Bearer {apiKey}", "User-Agent": "Python-urllib", } @@ -246,7 +246,7 @@ def getAvailableModels(apiKey: str) -> List[Dict[str, object]]: models = data.get("data", []) - availableModels: List[Dict[str, object]] = [] + availableModels: list[dict[str, object]] = [] for m in models: if m.get("deprecated", False): @@ -255,25 +255,27 @@ def getAvailableModels(apiKey: str) -> List[Dict[str, object]]: if not m.get("top_provider") or not m.get("context_length"): continue - availableModels.append({ - "id": m.get("id"), - "promptPricing": float(m.get("pricing", {}).get("prompt", 0)), - "completionPricing": float(m.get("pricing", {}).get("completion", 0)), - "contextLength": m.get("context_length"), - "deprecated": m.get("deprecated", False), - }) + availableModels.append( + { + "id": m.get("id"), + "promptPricing": float(m.get("pricing", {}).get("prompt", 0)), + "completionPricing": float(m.get("pricing", {}).get("completion", 0)), + "contextLength": m.get("context_length"), + "deprecated": m.get("deprecated", False), + }, + ) return availableModels -def _sendRequest(url: str, headers: Dict[str, str], data: Dict) -> str: +def _sendRequest(url: str, headers: dict[str, str], data: dict[str, Any]) -> str: """ Send an HTTP POST request to OpenRouter. Args: url (str): Endpoint URL. - headers (Dict[str, str]): HTTP headers. - data (Dict): JSON payload. + headers (dict[str, str]): HTTP headers. + data (dict[str, Any]): JSON payload. Returns: str: Assistant response text. @@ -326,12 +328,12 @@ def getHistory(filename: str) -> str: str: HTML-formatted conversation history, or an empty string if no history exists. """ - historyLines: List[str] = [] - allChat: List[Dict[str, str]] = [] + historyLines: list[str] = [] + allChat: list[dict[str, str]] = [] # Translators: Message announcing what the user said. - userQuestion: str = _('You said:') + userQuestion: str = _("You said:") # Translators: Message announcing what the model responded. - modelResponse: str = _('Model replied:') + modelResponse: str = _("Model replied:") if os.path.exists(filename): with open(filename, "rb") as f: @@ -375,12 +377,15 @@ def askOpenRouter(prompt: str, apiKey: str, new: bool = True) -> None: url: str = "https://openrouter.ai/api/v1/chat/completions" - addonPath: str = addonHandler.getCodeAddon().path + # Apply typing fix matching dialogs.py to avoid unknown member and variable types + addonPath: str = cast(Any, addonHandler).getCodeAddon().path historyFile: str = os.path.join(addonPath, "open_router_history.pkl") modelFile: str = os.path.join(addonPath, "model.txt") - useAll: bool = config.conf["askOpenRouter"].get("useAllModels", False) - selectedModel: str = config.conf["askOpenRouter"].get("selectedModel", "") + # Cast configuration elements to dictionary to solve dictionary access type errors + conf_dict = cast(dict[str, Any], config.conf["askOpenRouter"]) + useAll: bool = conf_dict.get("useAllModels", False) + selectedModel: str = conf_dict.get("selectedModel", "") # Reset conversation if requested if new: @@ -405,7 +410,7 @@ def askOpenRouter(prompt: str, apiKey: str, new: bool = True) -> None: # Translators: Message informing that no free models are available. _("No free model available at the moment."), # Translators: Title of the error message. - title=_("Model Error") + title=_("Model Error"), ) return @@ -423,32 +428,34 @@ def askOpenRouter(prompt: str, apiKey: str, new: bool = True) -> None: # Translators: Message informing that no free models are available. _("No free model available at the moment."), # Translators: Title of the error message. - title=_("Model Error") + title=_("Model Error"), ) return - history: List[Dict[str, str]] = loadHistory(historyFile) + history: list[dict[str, str]] = loadHistory(historyFile) - history.append({ - "role": "user", - "content": prompt - }) + history.append( + { + "role": "user", + "content": prompt, + }, + ) - headers: Dict[str, str] = { + headers: dict[str, str] = { "Authorization": f"Bearer {apiKey}", "Content-Type": "application/json", "HTTP-Referer": "http://localhost", - "X-Title": "My question" + "X-Title": "My question", } - data: Dict[str, Any] = { + data: dict[str, Any] = { "model": model, - "messages": history + "messages": history, } maxAttempts: int = 5 attempt: int = 0 - answer: Optional[str] = None + answer: str | None = None while attempt < maxAttempts: try: @@ -456,13 +463,12 @@ def askOpenRouter(prompt: str, apiKey: str, new: bool = True) -> None: break except urllib.error.HTTPError as e: - # If the user uses their own paid model, do not fallback if useAll: ui.browseableMessage( f"HTTP Error: {e.code}, {e.read().decode('utf-8')}", # Translators: Title of the HTTP error message. - title=_("HTTP Error") + title=_("HTTP Error"), ) return @@ -483,7 +489,7 @@ def askOpenRouter(prompt: str, apiKey: str, new: bool = True) -> None: ui.browseableMessage( f"HTTP Error: {e.code}, {e.read().decode('utf-8')}", # Translators: Title of the HTTP error message. - title=_("HTTP Error") + title=_("HTTP Error"), ) return @@ -491,7 +497,7 @@ def askOpenRouter(prompt: str, apiKey: str, new: bool = True) -> None: ui.browseableMessage( # Translators: Network error message. message=f"{_('Network error:')} {e.reason}", - title="Network Error" + title="Network Error", ) return @@ -500,20 +506,22 @@ def askOpenRouter(prompt: str, apiKey: str, new: bool = True) -> None: # Translators: Message informing that no free models are available at the moment. _("All free models are currently unavailable. Please try again later."), # Translators: Title of the model unavailable error. - title=_("Model Unavailable") + title=_("Model Unavailable"), ) return - history.append({ - "role": "assistant", - "content": answer - }) + history.append( + { + "role": "assistant", + "content": answer, + }, + ) saveHistory(history, historyFile) answerHtml: str = markdownToHtml(answer) - if config.conf["askOpenRouter"]["fullHistory"]: + if conf_dict["fullHistory"]: messageToDisplay: str = getHistory(historyFile) else: messageToDisplay = answerHtml @@ -523,14 +531,14 @@ def askOpenRouter(prompt: str, apiKey: str, new: bool = True) -> None: # Translators: Title of the model response message. title=_("Model Response"), isHtml=True, - copyButton=True + copyButton=True, ) def inputBox( - title: str, - func: Callable[[str, str, bool], None], - new: bool = True + title: str, + func: Callable[[str, str, bool], None], + new: bool = True, ) -> None: """ Display a multiline input dialog and send the result to the given function. @@ -555,7 +563,7 @@ def inputBox( # Translators: Message inviting the user to enter his question. _("Please enter the question you want to ask OpenRouter"), title, - style=wx.TE_MULTILINE | wx.OK | wx.CANCEL + style=wx.TE_MULTILINE | wx.OK | wx.CANCEL, ) def callback(result: int) -> None: @@ -565,25 +573,26 @@ def callback(result: int) -> None: # Translators: Message informing the user that the field is empty, he must fill it in. message=_("You did not enter anything. Please try again."), # Translators: Title of the error message. - caption=_("Input Error") + caption=_("Input Error"), ) return - apiKey: str = config.conf["askOpenRouter"]["apiKey"] + conf_dict = cast(dict[str, Any], config.conf["askOpenRouter"]) + apiKey: str = conf_dict["apiKey"] if not apiKey.strip(): gui.messageBox( # Translators: Message informing the user that no API key is configured. message=_("No API key is configured. Please configure it in settings."), # Translators: Title of the error message. - caption=_("Configuration Error") + caption=_("Configuration Error"), ) return func( dialog.Value, apiKey, - new + new, ) gui.runScriptModalDialog(dialog, callback) diff --git a/addon/locale/uk/LC_MESSAGES/nvda.po b/addon/locale/uk/LC_MESSAGES/nvda.po new file mode 100644 index 0000000..ad84e5e --- /dev/null +++ b/addon/locale/uk/LC_MESSAGES/nvda.po @@ -0,0 +1,202 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"Report-Msgid-Bugs-To: 'nvda-translations@groups.io'\n" +"POT-Creation-Date: 2026-03-27 09:01+0200\n" +"PO-Revision-Date: 2026-03-28 17:06+0200\n" +"Last-Translator: Volodymyr Pyrih \n" +"Language-Team: \n" +"Language: uk\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: Poedit 3.9\n" +"X-Poedit-Basepath: .\n" + +#. Translators: Title of the dialog box. +#: addon\globalPlugins\askOpenRouter\dialogs.py:54 +msgid "Chat Manager" +msgstr "Менеджер чату" + +#. Translators: Prompt label to create a new chat. +#: addon\globalPlugins\askOpenRouter\dialogs.py:69 +msgid "C&reate a New Chat" +msgstr "С&творити новий чат" + +#. Translators: Prompt label to continue an existing chat. +#: addon\globalPlugins\askOpenRouter\dialogs.py:75 +msgid "Co&ntinue a Chat" +msgstr "&Продовжити чат" + +#. Translators: Label of the closing button. +#: addon\globalPlugins\askOpenRouter\dialogs.py:81 +msgid "&Close" +msgstr "&Закрити" + +#. Translators: Title of the dialog box to start a new chat. +#: addon\globalPlugins\askOpenRouter\dialogs.py:118 +msgid "New Chat" +msgstr "Новий чат" + +#. Translators: Title of the dialog box to continue an existing chat. +#: addon\globalPlugins\askOpenRouter\dialogs.py:128 +msgid "Continue Chat" +msgstr "Продовжити чат" + +#. Translators: Label of the field that must contain the OpenRouter API key. +#: addon\globalPlugins\askOpenRouter\dialogs.py:167 +msgid "OpenRouter API Key:" +msgstr "Ключ API OpenRouter:" + +#. Translators: Label of the checkbox to display the OpenRouter API key. +#: addon\globalPlugins\askOpenRouter\dialogs.py:188 +msgid "Show API key" +msgstr "Показати ключ API" + +#. Translators: Label of the checkbox to display chat history. +#: addon\globalPlugins\askOpenRouter\dialogs.py:204 +msgid "Display the full chat history for continuous discussions" +msgstr "Показувати всю історію чату для безперервної розмови" + +#. Translators: +#. Translators: Label of the checkbox to show the list of models, including paid ones. +#: addon\globalPlugins\askOpenRouter\dialogs.py:220 +msgid "Use all models, including paid ones." +msgstr "Використовувати всі моделі, включаючи платні." + +#. Translators: Message informing that no free model is available. +#: addon\globalPlugins\askOpenRouter\functions.py:201 +msgid "No free model currently available." +msgstr "Зараз немає безкоштовної моделі." + +#. Translators: Message announcing what the user said. +#: addon\globalPlugins\askOpenRouter\functions.py:332 +msgid "You said:" +msgstr "Ви сказали:" + +#. Translators: Message announcing what the model responded. +#: addon\globalPlugins\askOpenRouter\functions.py:334 +msgid "Model replied:" +msgstr "Відповідь моделі:" + +#. Translators: Message informing that no free models are available. +#: addon\globalPlugins\askOpenRouter\functions.py:406 +#: addon\globalPlugins\askOpenRouter\functions.py:424 +msgid "No free model available at the moment." +msgstr "Наразі безкоштовна модель недоступна." + +#. Translators: Title of the error message. +#: addon\globalPlugins\askOpenRouter\functions.py:408 +#: addon\globalPlugins\askOpenRouter\functions.py:426 +msgid "Model Error" +msgstr "Помилка моделі" + +#. Translators: Title of the HTTP error message. +#: addon\globalPlugins\askOpenRouter\functions.py:465 +#: addon\globalPlugins\askOpenRouter\functions.py:486 +msgid "HTTP Error" +msgstr "Помилка HTTP" + +#. Translators: Network error message. +#: addon\globalPlugins\askOpenRouter\functions.py:493 +msgid "Network error:" +msgstr "Помилка мережі:" + +#. Translators: Message informing that no free models are available at the moment. +#: addon\globalPlugins\askOpenRouter\functions.py:501 +msgid "All free models are currently unavailable. Please try again later." +msgstr "Усі безкоштовні моделі наразі недоступні. Спробуйте пізніше." + +#. Translators: Title of the model unavailable error. +#: addon\globalPlugins\askOpenRouter\functions.py:503 +msgid "Model Unavailable" +msgstr "Модель недоступна" + +#. Translators: Title of the model response message. +#: addon\globalPlugins\askOpenRouter\functions.py:524 +msgid "Model Response" +msgstr "Відповідь моделі" + +#. Translators: Message inviting the user to enter his question. +#: addon\globalPlugins\askOpenRouter\functions.py:556 +msgid "Please enter the question you want to ask OpenRouter" +msgstr "Будь ласка, введіть запитання, яке хочете поставити OpenRouter" + +#. Translators: Message informing the user that the field is empty, he must fill it in. +#: addon\globalPlugins\askOpenRouter\functions.py:566 +msgid "You did not enter anything. Please try again." +msgstr "Ви нічого не ввели. Спробуйте ще раз." + +#. Translators: Title of the error message. +#: addon\globalPlugins\askOpenRouter\functions.py:568 +msgid "Input Error" +msgstr "Помилка введення" + +#. Translators: Message informing the user that no API key is configured. +#: addon\globalPlugins\askOpenRouter\functions.py:577 +msgid "No API key is configured. Please configure it in settings." +msgstr "Ключ API не налаштовано. Будь ласка, зробіть це в налаштуваннях." + +#. Translators: Title of the error message. +#: addon\globalPlugins\askOpenRouter\functions.py:579 +msgid "Configuration Error" +msgstr "Помилка конфігурації" + +#. Translators: Description of the script which opens the interaction dialog box with OpenRouter. +#: addon\globalPlugins\askOpenRouter\__init__.py:57 +msgid "Opens a dialog allowing the user to ask questions to OpenRouter" +msgstr "Відкриває діалог, у якому можна ставити запитання OpenRouter" + +#. Translators: Description of the script which allows to create a new chat. +#: addon\globalPlugins\askOpenRouter\__init__.py:65 +msgid "Opens the prompt to start a new OpenRouter chat." +msgstr "Відкриває підказку для початку нового чату в OpenRouter." + +#. Translators: Description of the script which allows to continue an existing chat. +#: addon\globalPlugins\askOpenRouter\__init__.py:73 +msgid "Opens the prompt to continue an existing OpenRouter chat." +msgstr "Відкриває запит на продовження наявного чату OpenRouter." + +#. Translators: Description of the script which allows to show OpenRouter settings panel.. +#: addon\globalPlugins\askOpenRouter\__init__.py:81 +msgid "Opens the add-on settings panel." +msgstr "Відкриває панель налаштувань додатка." + +#. Add-on summary/title, usually the user visible name of the add-on +#. Translators: Summary/title for this add-on +#. to be shown on installation and add-on information found in add-on store +#: buildVars.py:21 +msgid "Ask OpenRouter" +msgstr "Запитати OpenRouter" + +#. Add-on description +#. Translators: Long description to be shown for this add-on on add-on information from add-on store +#: buildVars.py:24 +msgid "" +"This add-on allows you to ask questions to Artificial Intelligence models " +"available on the OpenRouter platform.\n" +"You can either use automatic random selection of free models or choose a " +"specific model manually, including paid ones.\n" +"To use this add-on, you must create an account on https://openrouter.ai and " +"generate an API key.\n" +"Once configured, you can open a dialog that lets you start a new " +"conversation or continue an existing one." +msgstr "" +"Цей додаток дозволяє ставити запитання моделям штучного інтелекту, " +"доступним на платформі OpenRouter.\n" +"Ви можете скористатися автоматичним випадковим вибором безкоштовних моделей " +"або вибрати конкретну модель вручну, в тому числі платну.\n" +"Щоб використовувати цей додаток, ви повинні створити обліковий запис на " +"https://openrouter.ai та створити ключ API.\n" +"Після налаштування можна відкрити діалог, за допомогою якого можна почати " +"нову розмову або продовжити наявну." + +#. Brief changelog for this version +#. Translators: what's new content for the add-on version to be shown in the add-on store +#: buildVars.py:32 +msgid "" +"Changes for 20260301.0.0\n" +"Updated documentation." +msgstr "" +"Зміни для 20260301.0.0\n" +"Оновлена документація." diff --git a/pyproject.toml b/pyproject.toml index 40acb8f..7de2cc1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,10 +32,11 @@ dependencies = [ "markdown-link-attr-modifier==0.2.1", "mdx-gh-links==0.4", # Lint - "uv==0.11.6", + "uv==0.11.15", "ruff==0.14.5", "pre-commit==4.2.0", - "pyright[nodejs]==1.1.407", + "pyright[nodejs]==1.1.411", + "wxpython>=4.2.5", ] [project.urls] Repository = "https://github.com/abdel792/askOpenRouter" diff --git a/uv.lock b/uv.lock index 7e17b67..c6f2a17 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = "==3.13.*" [[package]] -name = "addontemplate" +name = "askopenrouter" source = { editable = "." } dependencies = [ { name = "crowdin-api-client" }, @@ -18,6 +18,7 @@ dependencies = [ { name = "ruff" }, { name = "scons" }, { name = "uv" }, + { name = "wxpython" }, ] [package.metadata] @@ -30,19 +31,20 @@ requires-dist = [ { name = "mdx-truly-sane-lists", specifier = "==1.3" }, { name = "nh3", specifier = "==0.3.2" }, { name = "pre-commit", specifier = "==4.2.0" }, - { name = "pyright", extras = ["nodejs"], specifier = "==1.1.407" }, + { name = "pyright", extras = ["nodejs"], specifier = "==1.1.411" }, { name = "ruff", specifier = "==0.14.5" }, { name = "scons", specifier = "==4.10.1" }, - { name = "uv", specifier = "==0.11.6" }, + { name = "uv", specifier = "==0.11.15" }, + { name = "wxpython", specifier = ">=4.2.5" }, ] [[package]] name = "certifi" -version = "2026.4.22" +version = "2026.5.20" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" }, ] [[package]] @@ -106,20 +108,20 @@ wheels = [ [[package]] name = "distlib" -version = "0.4.0" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +sdist = { url = "https://files.pythonhosted.org/packages/46/8d/873e9252ea2c0e0c857884e0a2899ec43ade132345df1925ef24cbe64f18/distlib-0.4.2.tar.gz", hash = "sha256:baeb401c90f27acd15c4861ae0847d1e731c27ac3dbf4210643ba61fa1e813db", size = 614914, upload-time = "2026-06-08T16:24:15.439Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, + { url = "https://files.pythonhosted.org/packages/c1/60/aa891c893821d4d127292ed66c6940d1d715894bd5a0ce048056bc641773/distlib-0.4.2-py2.py3-none-any.whl", hash = "sha256:ca4cb11e5d746b5ec13c199cbf19ae27a241f89702b54e153a74332955446067", size = 470510, upload-time = "2026-06-08T16:24:13.208Z" }, ] [[package]] name = "filelock" -version = "3.29.0" +version = "3.29.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/f9/f38573ed5844586db374d085911740a501ccfa373b455fc9413f09f85237/filelock-3.29.1.tar.gz", hash = "sha256:d97e6b1b9757569626c58caa07dc4beb1613f4a2938b1e8cc81afca398906c9e", size = 59335, upload-time = "2026-06-03T15:19:04.053Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, + { url = "https://files.pythonhosted.org/packages/4c/a0/614c5fe402fd88951df45f4dda2fa3b4e17a99ecd92340771929169b3b95/filelock-3.29.1-py3-none-any.whl", hash = "sha256:85199dfd706869641b72b2e8955d5416a4b2b7dc4b0e8e6d97b4cc1299a6983b", size = 40750, upload-time = "2026-06-03T15:19:02.959Z" }, ] [[package]] @@ -133,11 +135,11 @@ wheels = [ [[package]] name = "idna" -version = "3.13" +version = "3.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/cc/762dfb036166873f0059f3b7de4565e1b5bc3d6f28a414c13da27e442f99/idna-3.13.tar.gz", hash = "sha256:585ea8fe5d69b9181ec1afba340451fba6ba764af97026f92a91d4eef164a242", size = 194210, upload-time = "2026-04-22T16:42:42.314Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/13/ad7d7ca3808a898b4612b6fe93cde56b53f3034dcde235acb1f0e1df24c6/idna-3.13-py3-none-any.whl", hash = "sha256:892ea0cde124a99ce773decba204c5552b69c3c67ffd5f232eb7696135bc8bb3", size = 68629, upload-time = "2026-04-22T16:42:40.909Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] [[package]] @@ -245,27 +247,27 @@ wheels = [ [[package]] name = "nodejs-wheel-binaries" -version = "24.15.0" +version = "24.16.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3f/70/a1e4f4d5986768ab90cc860b1cc3660fd2ded74ca175a900a5c29f839c7d/nodejs_wheel_binaries-24.15.0.tar.gz", hash = "sha256:b43f5c4f6e5768d8845b2ae4682eb703a19bf7aadc84187e2d903ed3a611c859", size = 8057, upload-time = "2026-04-19T15:48:16.899Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/22/2a5beb4e21417c73233d9f65cf6f3e96e891b80d2f550a8f630ebc6b88c6/nodejs_wheel_binaries-24.16.0.tar.gz", hash = "sha256:c973cb69dc5fd16e6f6dc6e579e2c3d5534e2a1f57619dddf5ba070efa7dde37", size = 8056, upload-time = "2026-05-30T16:52:09.807Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/85/66/54051d14853d6ab4fb85f8be9b042b530be653357fb9a19557498bc91ab7/nodejs_wheel_binaries-24.15.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:a6232fa8b754220941f52388c8ead923f7c1c7fdf0ea0d98f657523bd9a81ef4", size = 55173485, upload-time = "2026-04-19T15:47:34.561Z" }, - { url = "https://files.pythonhosted.org/packages/ad/5f/66acada164da5ca10a0824db021aa7394ae18396c550cd9280e839a43126/nodejs_wheel_binaries-24.15.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:001a6b62c69d9109c1738163cca00608dd2722e8663af59300054ea02610972d", size = 55348100, upload-time = "2026-04-19T15:47:40.521Z" }, - { url = "https://files.pythonhosted.org/packages/0d/2d/0cbd5ff40c9bb030ca1735d8f8793bd74f08a4cbd49100a1d19313ea57ab/nodejs_wheel_binaries-24.15.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:0fbc48765e60ed0ff30d43898dbf5cadbadf2e5f1e7f204afc2b01493b7ebce6", size = 59668206, upload-time = "2026-04-19T15:47:46.848Z" }, - { url = "https://files.pythonhosted.org/packages/da/d5/91ac63951ec75927a486b83b8cafe650e360fa70ac01dc94adfb32b93b97/nodejs_wheel_binaries-24.15.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:20ee0536809795da8a4942fc1ab4cbdebbcaaf29383eab67ba8874268fb00008", size = 60206736, upload-time = "2026-04-19T15:47:52.668Z" }, - { url = "https://files.pythonhosted.org/packages/db/72/dc22776974d928869c0c30d23ee98ed7df254243c2df68f09f5963e8e8b8/nodejs_wheel_binaries-24.15.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1fade6c214285e72472ca40a631e98ff36559671cd5eefc8bf009471d67f04b4", size = 61720456, upload-time = "2026-04-19T15:47:58.325Z" }, - { url = "https://files.pythonhosted.org/packages/01/0a/34461b9050cb45ee371dccdefc622aef6351506ea2691b08fc761ca67150/nodejs_wheel_binaries-24.15.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3984cb8d87766567aee67a49743227ab40ede6f47734ec990ff90e50b74e7740", size = 62326172, upload-time = "2026-04-19T15:48:04.094Z" }, - { url = "https://files.pythonhosted.org/packages/c9/17/09252bf35672dba926649d59dfe51443a0f6955ad13784e91131d5ec82a2/nodejs_wheel_binaries-24.15.0-py2.py3-none-win_amd64.whl", hash = "sha256:a437601956b532dcb3082046e6978e622733f90edc0932cbb9adb3bb97a16501", size = 41543461, upload-time = "2026-04-19T15:48:09.332Z" }, - { url = "https://files.pythonhosted.org/packages/0a/7e/b649777d148e1e0c2ce349156603cdb12f7ed99921b95d93717393650193/nodejs_wheel_binaries-24.15.0-py2.py3-none-win_arm64.whl", hash = "sha256:bdf4a431e08321a32efc604111c6f23941f87055d796a537e8c4110daecad23f", size = 39233248, upload-time = "2026-04-19T15:48:13.326Z" }, + { url = "https://files.pythonhosted.org/packages/83/d1/68b43b53cd0fa83ae6fd406705023ca988d9e0ca41c724d82e66fbeb2ef6/nodejs_wheel_binaries-24.16.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:d9f8f677dcf30e37ac244f07869726abe043f01eb0f45722b1df31cc2af7093c", size = 55666374, upload-time = "2026-05-30T16:51:39.588Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b2/40a989159599080da485de966c4c2d207e852ac7aa7864702626d96c8bf5/nodejs_wheel_binaries-24.16.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:3d0370fe7120ce9697a4f60d40480d2bd8808d9f30131458d5afc0040d4e5a51", size = 55838487, upload-time = "2026-05-30T16:51:43.383Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a7/cd42174fb5ff6faff7fa8d326a18914d8f232098ab5de055b57c16fa13ca/nodejs_wheel_binaries-24.16.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:85dc92bbb79c851569c5925dcc2a4c915a034efab375f99e4e7e6bbe9cca8342", size = 60179540, upload-time = "2026-05-30T16:51:47.036Z" }, + { url = "https://files.pythonhosted.org/packages/2b/95/c8a1f9ae140aa28df8744d984d01d4b3af7cdd6555af12127f40ceb45a7d/nodejs_wheel_binaries-24.16.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:2f3036292811514ba847b3708492644764f88a833ac425c5f55007014308ddfd", size = 60716262, upload-time = "2026-05-30T16:51:50.711Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/7c35b3737f59e36d0249c265397b7bff570519b95301d6e16ea361e904ad/nodejs_wheel_binaries-24.16.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:db8a8a76ebd2b28ecbfc9ad464baa3707241b9e050a30e2efdf6f60c0f886502", size = 62230592, upload-time = "2026-05-30T16:51:55Z" }, + { url = "https://files.pythonhosted.org/packages/04/96/d931255cf9d11a84d6b54d882dba7434646467d568ccf070ea3418638df3/nodejs_wheel_binaries-24.16.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f1a3d8f7b4491cbbd023ba3fc4e901fcca2d9fb80d57f24ba3890de8b1dbac03", size = 62841759, upload-time = "2026-05-30T16:51:59.407Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7b/8b7a3f41bc255411be30b6d7d288aab8ffd9ea2055db8555ced3548007b9/nodejs_wheel_binaries-24.16.0-py2.py3-none-win_amd64.whl", hash = "sha256:bb136be9944f0662dcf1120f45193a6b75b13fac378971a95cc42c9f879a81aa", size = 42027734, upload-time = "2026-05-30T16:52:03.348Z" }, + { url = "https://files.pythonhosted.org/packages/17/66/1ed71f1f529b8ca727d42c7ceb9db0bef145ce4a13dfc86fb50aa44f3be6/nodejs_wheel_binaries-24.16.0-py2.py3-none-win_arm64.whl", hash = "sha256:8308940b5edd0a50dc5267ea36ba21c9f668e83fe0d9f293937174d3a7e31c36", size = 39714528, upload-time = "2026-05-30T16:52:06.421Z" }, ] [[package]] name = "platformdirs" -version = "4.9.6" +version = "4.10.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, + { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" }, ] [[package]] @@ -286,15 +288,15 @@ wheels = [ [[package]] name = "pyright" -version = "1.1.407" +version = "1.1.411" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "nodeenv" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a6/1b/0aa08ee42948b61745ac5b5b5ccaec4669e8884b53d31c8ec20b2fcd6b6f/pyright-1.1.407.tar.gz", hash = "sha256:099674dba5c10489832d4a4b2d302636152a9a42d317986c38474c76fe562262", size = 4122872, upload-time = "2025-10-24T23:17:15.145Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/ab/265f7dc69d28113ebba19092e57b075f41543b2ed048429c5f56e2b88eac/pyright-1.1.411.tar.gz", hash = "sha256:d885a0551f2e763b089a02702174e7f4ba77548cddabc972ab86d1f7f1b0f998", size = 4112861, upload-time = "2026-06-25T02:14:06.37Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/93/b69052907d032b00c40cb656d21438ec00b3a471733de137a3f65a49a0a0/pyright-1.1.407-py3-none-any.whl", hash = "sha256:6dd419f54fcc13f03b52285796d65e639786373f433e243f8b94cf93a7444d21", size = 5997008, upload-time = "2025-10-24T23:17:13.159Z" }, + { url = "https://files.pythonhosted.org/packages/0a/49/385be530a6a5b78d1cbcd5c2e38debc8959a2fc6bdb716f4e581002979fc/pyright-1.1.411-py3-none-any.whl", hash = "sha256:dc7c72a8e2700c55baa127554040e067041ea53ccfd50bf96308cc4291c7d5d9", size = 6181526, upload-time = "2026-06-25T02:14:04.691Z" }, ] [package.optional-dependencies] @@ -304,15 +306,15 @@ nodejs = [ [[package]] name = "python-discovery" -version = "1.2.2" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "filelock" }, { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/12/38c1a0b1e64806780c9563e3fc9f6e472251839662587cfbe9bfaf2ae10a/python_discovery-1.4.0.tar.gz", hash = "sha256:eb8bc7daad3c226c147e45bb4e970a1feb1bf4048ee178e6db59e197b8010ce3", size = 68455, upload-time = "2026-05-28T01:15:37.639Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8d/3d316429f65029532bb1e28ff77b797d86b5ac3915bb44ca4e19aa283d43/python_discovery-1.4.0-py3-none-any.whl", hash = "sha256:26ed78d703e234879a66244c7d4114563fb13ec5cd30a2d1357e5fb4850782da", size = 33217, upload-time = "2026-05-28T01:15:36.573Z" }, ] [[package]] @@ -335,7 +337,7 @@ wheels = [ [[package]] name = "requests" -version = "2.33.0" +version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -343,9 +345,9 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/34/64/8860370b167a9721e8956ae116825caff829224fbca0ca6e7bf8ddef8430/requests-2.33.0.tar.gz", hash = "sha256:c7ebc5e8b0f21837386ad0e1c8fe8b829fa5f544d8df3b2253bff14ef29d7652", size = 134232, upload-time = "2026-03-25T15:10:41.586Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/56/5d/c814546c2333ceea4ba42262d8c4d55763003e767fa169adc693bd524478/requests-2.33.0-py3-none-any.whl", hash = "sha256:3324635456fa185245e24865e810cecec7b4caf933d7eb133dcde67d48cee69b", size = 65017, upload-time = "2026-03-25T15:10:40.382Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] @@ -394,42 +396,42 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] name = "uv" -version = "0.11.6" +version = "0.11.15" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/dd/f3/8aceeab67ea69805293ab290e7ca8cc1b61a064d28b8a35c76d8eba063dd/uv-0.11.6.tar.gz", hash = "sha256:e3b21b7e80024c95ff339fcd147ac6fc3dd98d3613c9d45d3a1f4fd1057f127b", size = 4073298, upload-time = "2026-04-09T12:09:01.738Z" } +sdist = { url = "https://files.pythonhosted.org/packages/da/34/609d5d01ba21dc8f0974610ca7802fbb2c946a0c38665cfe5c5aeddbefb5/uv-0.11.15.tar.gz", hash = "sha256:755f959ec6a2fd8ccb6ee76ad90ab759d2eb1f4797444078645dd1ee4bca92d6", size = 4159545, upload-time = "2026-05-18T19:57:48.133Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/fe/4b61a3d5ad9d02e8a4405026ccd43593d7044598e0fa47d892d4dafe44c9/uv-0.11.6-py3-none-linux_armv6l.whl", hash = "sha256:ada04dcf89ddea5b69d27ac9cdc5ef575a82f90a209a1392e930de504b2321d6", size = 23780079, upload-time = "2026-04-09T12:08:56.609Z" }, - { url = "https://files.pythonhosted.org/packages/52/db/d27519a9e1a5ffee9d71af1a811ad0e19ce7ab9ae815453bef39dd479389/uv-0.11.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5be013888420f96879c6e0d3081e7bcf51b539b034a01777041934457dfbedf3", size = 23214721, upload-time = "2026-04-09T12:09:32.228Z" }, - { url = "https://files.pythonhosted.org/packages/a6/8f/4399fa8b882bd7e0efffc829f73ab24d117d490a93e6bc7104a50282b854/uv-0.11.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:ffa5dc1cbb52bdce3b8447e83d1601a57ad4da6b523d77d4b47366db8b1ceb18", size = 21750109, upload-time = "2026-04-09T12:09:24.357Z" }, - { url = "https://files.pythonhosted.org/packages/32/07/5a12944c31c3dda253632da7a363edddb869ed47839d4d92a2dc5f546c93/uv-0.11.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:bfb107b4dade1d2c9e572992b06992d51dd5f2136eb8ceee9e62dd124289e825", size = 23551146, upload-time = "2026-04-09T12:09:10.439Z" }, - { url = "https://files.pythonhosted.org/packages/79/5b/2ec8b0af80acd1016ed596baf205ddc77b19ece288473b01926c4a9cf6db/uv-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:9e2fe7ce12161d8016b7deb1eaad7905a76ff7afec13383333ca75e0c4b5425d", size = 23331192, upload-time = "2026-04-09T12:09:34.792Z" }, - { url = "https://files.pythonhosted.org/packages/62/7d/eea35935f2112b21c296a3e42645f3e4b1aa8bcd34dcf13345fbd55134b7/uv-0.11.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ed9c6f70c25e8dfeedddf4eddaf14d353f5e6b0eb43da9a14d3a1033d51d915", size = 23337686, upload-time = "2026-04-09T12:09:18.522Z" }, - { url = "https://files.pythonhosted.org/packages/21/47/2584f5ab618f6ebe9bdefb2f765f2ca8540e9d739667606a916b35449eec/uv-0.11.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d68a013e609cebf82077cbeeb0809ed5e205257814273bfd31e02fc0353bbfc2", size = 25008139, upload-time = "2026-04-09T12:09:03.983Z" }, - { url = "https://files.pythonhosted.org/packages/95/81/497ae5c1d36355b56b97dc59f550c7e89d0291c163a3f203c6f341dff195/uv-0.11.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:93f736dddca03dae732c6fdea177328d3bc4bf137c75248f3d433c57416a4311", size = 25712458, upload-time = "2026-04-09T12:09:07.598Z" }, - { url = "https://files.pythonhosted.org/packages/3c/1c/74083238e4fab2672b63575b9008f1ea418b02a714bcfcf017f4f6a309b6/uv-0.11.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e96a66abe53fced0e3389008b8d2eff8278cfa8bb545d75631ae8ceb9c929aba", size = 24915507, upload-time = "2026-04-09T12:08:50.892Z" }, - { url = "https://files.pythonhosted.org/packages/5a/ee/e14fe10ba455a823ed18233f12de6699a601890905420b5c504abf115116/uv-0.11.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b096311b2743b228df911a19532b3f18fa420bf9530547aecd6a8e04bbfaccd", size = 24971011, upload-time = "2026-04-09T12:08:54.016Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/7b9c83eaadf98e343317ff6384a7227a4855afd02cdaf9696bcc71ee6155/uv-0.11.6-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:904d537b4a6e798015b4a64ff5622023bd4601b43b6cd1e5f423d63471f5e948", size = 23640234, upload-time = "2026-04-09T12:09:15.735Z" }, - { url = "https://files.pythonhosted.org/packages/d6/51/75ccdd23e76ff1703b70eb82881cd5b4d2a954c9679f8ef7e0136ef2cfab/uv-0.11.6-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:4ed8150c26b5e319381d75ae2ce6aba1e9c65888f4850f4e3b3fa839953c90a5", size = 24452664, upload-time = "2026-04-09T12:09:26.875Z" }, - { url = "https://files.pythonhosted.org/packages/4d/86/ace80fe47d8d48b5e3b5aee0b6eb1a49deaacc2313782870250b3faa36f5/uv-0.11.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1c9218c8d4ac35ca6e617fb0951cc0ab2d907c91a6aea2617de0a5494cf162c0", size = 24494599, upload-time = "2026-04-09T12:09:37.368Z" }, - { url = "https://files.pythonhosted.org/packages/05/2d/4b642669b56648194f026de79bc992cbfc3ac2318b0a8d435f3c284934e8/uv-0.11.6-py3-none-musllinux_1_1_i686.whl", hash = "sha256:9e211c83cc890c569b86a4183fcf5f8b6f0c7adc33a839b699a98d30f1310d3a", size = 24159150, upload-time = "2026-04-09T12:09:13.17Z" }, - { url = "https://files.pythonhosted.org/packages/ae/24/7eecd76fe983a74fed1fc700a14882e70c4e857f1d562a9f2303d4286c12/uv-0.11.6-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:d2a1d2089afdf117ad19a4c1dd36b8189c00ae1ad4135d3bfbfced82342595cf", size = 25164324, upload-time = "2026-04-09T12:08:59.56Z" }, - { url = "https://files.pythonhosted.org/packages/27/e0/bbd4ba7c2e5067bbba617d87d306ec146889edaeeaa2081d3e122178ca08/uv-0.11.6-py3-none-win32.whl", hash = "sha256:6e8344f38fa29f85dcfd3e62dc35a700d2448f8e90381077ef393438dcd5012e", size = 22865693, upload-time = "2026-04-09T12:09:21.415Z" }, - { url = "https://files.pythonhosted.org/packages/a5/33/1983ce113c538a856f2d620d16e39691962ecceef091a84086c5785e32e5/uv-0.11.6-py3-none-win_amd64.whl", hash = "sha256:a28bea69c1186303d1200f155c7a28c449f8a4431e458fcf89360cc7ef546e40", size = 25371258, upload-time = "2026-04-09T12:09:40.52Z" }, - { url = "https://files.pythonhosted.org/packages/35/01/be0873f44b9c9bc250fcbf263367fcfc1f59feab996355bcb6b52fff080d/uv-0.11.6-py3-none-win_arm64.whl", hash = "sha256:a78f6d64b9950e24061bc7ec7f15ff8089ad7f5a976e7b65fcadce58fe02f613", size = 23869585, upload-time = "2026-04-09T12:09:29.425Z" }, + { url = "https://files.pythonhosted.org/packages/a6/7c/dcc230c5911884d8848145dabcac8fb95a5ed6f9fe1c57fae8242618f28a/uv-0.11.15-py3-none-linux_armv6l.whl", hash = "sha256:83b04ab49514a0a761ffedb36a748ee81f87746671e72088e5f32c9585e5f1a9", size = 23110183, upload-time = "2026-05-18T19:57:23.051Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f3/efd4e044b60eb9c3c12ee386be098d56c335538ccec7caa49349cfba9344/uv-0.11.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6cae61f737be075b90be9e3f07d961072aed7019f4c9b8ed5c5d41c4d6cade3", size = 22637941, upload-time = "2026-05-18T19:57:26.752Z" }, + { url = "https://files.pythonhosted.org/packages/a6/b8/48627f895a1569e576822e0a8416aa4797eb4a4551de21a4ad97b9b5819d/uv-0.11.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:9accae33619a9166e5c48531deb455d672cfb89f9357a00975e669c76b0bd49f", size = 21258803, upload-time = "2026-05-18T19:57:05.473Z" }, + { url = "https://files.pythonhosted.org/packages/af/50/4bc8a148274feabee2d9c9f1fa15009e10c0228dfe57981ee3ea2ef1d481/uv-0.11.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:c0cf52cd6d50bb9e05e2d968f45f80761107e4cbc8d4a26d9758f9d8274aaec1", size = 23066178, upload-time = "2026-05-18T19:57:33.058Z" }, + { url = "https://files.pythonhosted.org/packages/a9/56/139fc3bec9a8b0a25bfe2196123adb9f16124da437bf4fbcf0d21cfcafb2/uv-0.11.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:49dc6ed70bff00937384f96cdc4b1a4742d18e5504ec2c4a1214dba2dee5687a", size = 22705332, upload-time = "2026-05-18T19:57:36.714Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b0/b18b3dd204f8c213236a1ebd148e009861637129a8cce34df0e9aa22ed40/uv-0.11.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:adb9a89352539fdd8f7cd5f9966cf9f94fc5b98e0ccdf5003a04123dc6423bec", size = 22707534, upload-time = "2026-05-18T19:58:04.117Z" }, + { url = "https://files.pythonhosted.org/packages/76/36/3ca09f95572df99d361b49c96b1297149e96e120d8d1ecf074095a4b6da4/uv-0.11.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40ff67e3f8e8a7533781a2e892a534975a93acb83ea35460e64e7b2bf2111774", size = 24096607, upload-time = "2026-05-18T19:58:11.625Z" }, + { url = "https://files.pythonhosted.org/packages/64/be/3bdee21a296bbf5336a526e3613d0e7d4538dacc39c62d7fcba55d15f6b0/uv-0.11.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c6463a299ed7e6b5a800ed6f108af8e1588352629424133ddef7572b0e1e1118", size = 25082562, upload-time = "2026-05-18T19:57:40.69Z" }, + { url = "https://files.pythonhosted.org/packages/cd/73/f371f3689ffe741066468d001d85f739fc4b5574de83b639ef19b5e8a7f4/uv-0.11.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:68c1e62d4b78578b90b833553286b65d6a7e327537716441068583ba652ec4f5", size = 24253391, upload-time = "2026-05-18T19:57:18.47Z" }, + { url = "https://files.pythonhosted.org/packages/d3/16/fe392d618af6b00c064b3e718d585dcf791546a77c5123a5bec07ce53a0a/uv-0.11.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98edf1bdaf82447014852051d93e3ee95012509c567bf057fd117e6bdbd9a807", size = 24415871, upload-time = "2026-05-18T19:58:19.651Z" }, + { url = "https://files.pythonhosted.org/packages/6e/24/2e92a052fb6334fcd746d1c7cb57847c204b118c84f5da53c0f9e129f7b7/uv-0.11.15-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:be8f76d25bcf4c92bb384240ac1bf9aa7f51063d0bdeca4c9cf0ec3ed8b145e0", size = 23159007, upload-time = "2026-05-18T19:57:10.653Z" }, + { url = "https://files.pythonhosted.org/packages/3d/2e/6923d0658d164bb2c435ed1868aa2d49b3074594679917a001ff92dc95bb/uv-0.11.15-py3-none-manylinux_2_31_riscv64.musllinux_1_1_riscv64.whl", hash = "sha256:f9f4fbbf4fe485522054f3c7496c6e8e932d6436e4200ff3daf718db0b7c7bd5", size = 23769385, upload-time = "2026-05-18T19:58:15.856Z" }, + { url = "https://files.pythonhosted.org/packages/a4/99/7e34cd949e57360814e8064cc9fb7104df445d0f6a663504e5f7473480aa/uv-0.11.15-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:0ed920e896b2fd13a35031707e307e42fbb2681458b967440a17272d86d49137", size = 23860973, upload-time = "2026-05-18T19:57:55.575Z" }, + { url = "https://files.pythonhosted.org/packages/28/98/8fe1f5f9d816e94569a0298dd8e0936801097625fa1952162951f0d628b6/uv-0.11.15-py3-none-musllinux_1_1_i686.whl", hash = "sha256:41d907611f3e6a13262807fd7f0a17849f76285ca80f536f6b3943732bdc6656", size = 23431392, upload-time = "2026-05-18T19:57:59.814Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6b/76a1ce2fa860026913a5941700cdc7d715fce9c3277a3fa3489cf2523ca0/uv-0.11.15-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:e3b68f8bf1a4568710f77e5bda9182ce7682811d89a8e7468c22460e032b234d", size = 24519478, upload-time = "2026-05-18T19:57:51.165Z" }, + { url = "https://files.pythonhosted.org/packages/43/60/1d58e8a05718cb50494763115710b73846cacb651fd735d285233fd72c59/uv-0.11.15-py3-none-win32.whl", hash = "sha256:8e2da3076761086a5b76869c3f38ef0509c836046ef41ddd19485dfd7271dca9", size = 22020178, upload-time = "2026-05-18T19:58:07.64Z" }, + { url = "https://files.pythonhosted.org/packages/55/53/40fcefcb348af660488597ed3c01363df7344e60611f8883750dc596f5c6/uv-0.11.15-py3-none-win_amd64.whl", hash = "sha256:cc3915ab291a1ecaf31de05f5d8bd70d09c66fe9911a53f70d9efa62ff0dbd8a", size = 24668779, upload-time = "2026-05-18T19:57:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7d/fa3a9960c95af9bbe2a629048760d0b9b4fead8ccd4f2235af747ec7cdf0/uv-0.11.15-py3-none-win_arm64.whl", hash = "sha256:4f39426a13dee24897aed60c4b98058c66f18bd983885ac5f4a54a04b24fbddf", size = 23198178, upload-time = "2026-05-18T19:57:14.68Z" }, ] [[package]] name = "virtualenv" -version = "21.2.4" +version = "21.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "distlib" }, @@ -437,38 +439,51 @@ dependencies = [ { name = "platformdirs" }, { name = "python-discovery" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0c/98/3a7e644e19cb26133488caff231be390579860bbbb3da35913c49a1d0a46/virtualenv-21.2.4.tar.gz", hash = "sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada", size = 5850742, upload-time = "2026-04-14T22:15:31.438Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e1/0d/4e93c8e6d1001a75763f87d8f5ecda8ebc7f4aa2153dddfaf4ae8892821a/virtualenv-21.4.2.tar.gz", hash = "sha256:38e6ee0a555615c0ea9da2ac7e9998fe8dc3b911dd33ad8eaad2020957653b0c", size = 7613326, upload-time = "2026-05-31T17:01:22.827Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl", hash = "sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac", size = 5831232, upload-time = "2026-04-14T22:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/bf/c4/557dc082be035381b85fdb2b74e21d3d21b57750b74f2b47a32f3a639ff9/virtualenv-21.4.2-py3-none-any.whl", hash = "sha256:854210ca524a1a4d0d744734f4acbc721c3ffe163b85bbf5d56d14d5ae2f0fae", size = 7594079, upload-time = "2026-05-31T17:01:20.735Z" }, ] [[package]] name = "wrapt" -version = "2.1.2" +version = "2.2.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/64/925f213fdcbb9baeb1530449ac71a4d57fc361c053d06bf78d0c5c7cd80c/wrapt-2.1.2.tar.gz", hash = "sha256:3996a67eecc2c68fd47b4e3c564405a5777367adfd9b8abb58387b63ee83b21e", size = 81678, upload-time = "2026-03-06T02:53:25.134Z" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/9f/06263fcd8ad6c405f05a3905fd7a84dd3176eb5ad46e44bccc0cd16348bb/wrapt-2.2.1.tar.gz", hash = "sha256:6744f504375775d7609c82c8d3d94af1c9a6f05586984536905908ba905277b9", size = 127620, upload-time = "2026-05-22T14:49:43.056Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4c/7a/d936840735c828b38d26a854e85d5338894cda544cb7a85a9d5b8b9c4df7/wrapt-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787fd6f4d67befa6fe2abdffcbd3de2d82dfc6fb8a6d850407c53332709d030b", size = 61259, upload-time = "2026-03-06T02:53:41.922Z" }, - { url = "https://files.pythonhosted.org/packages/5e/88/9a9b9a90ac8ca11c2fdb6a286cb3a1fc7dd774c00ed70929a6434f6bc634/wrapt-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4bdf26e03e6d0da3f0e9422fd36bcebf7bc0eeb55fdf9c727a09abc6b9fe472e", size = 61851, upload-time = "2026-03-06T02:52:48.672Z" }, - { url = "https://files.pythonhosted.org/packages/03/a9/5b7d6a16fd6533fed2756900fc8fc923f678179aea62ada6d65c92718c00/wrapt-2.1.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bbac24d879aa22998e87f6b3f481a5216311e7d53c7db87f189a7a0266dafffb", size = 121446, upload-time = "2026-03-06T02:54:14.013Z" }, - { url = "https://files.pythonhosted.org/packages/45/bb/34c443690c847835cfe9f892be78c533d4f32366ad2888972c094a897e39/wrapt-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:16997dfb9d67addc2e3f41b62a104341e80cac52f91110dece393923c0ebd5ca", size = 123056, upload-time = "2026-03-06T02:54:10.829Z" }, - { url = "https://files.pythonhosted.org/packages/93/b9/ff205f391cb708f67f41ea148545f2b53ff543a7ac293b30d178af4d2271/wrapt-2.1.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:162e4e2ba7542da9027821cb6e7c5e068d64f9a10b5f15512ea28e954893a267", size = 117359, upload-time = "2026-03-06T02:53:03.623Z" }, - { url = "https://files.pythonhosted.org/packages/1f/3d/1ea04d7747825119c3c9a5e0874a40b33594ada92e5649347c457d982805/wrapt-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f29c827a8d9936ac320746747a016c4bc66ef639f5cd0d32df24f5eacbf9c69f", size = 121479, upload-time = "2026-03-06T02:53:45.844Z" }, - { url = "https://files.pythonhosted.org/packages/78/cc/ee3a011920c7a023b25e8df26f306b2484a531ab84ca5c96260a73de76c0/wrapt-2.1.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a9dd9813825f7ecb018c17fd147a01845eb330254dff86d3b5816f20f4d6aaf8", size = 116271, upload-time = "2026-03-06T02:54:46.356Z" }, - { url = "https://files.pythonhosted.org/packages/98/fd/e5ff7ded41b76d802cf1191288473e850d24ba2e39a6ec540f21ae3b57cb/wrapt-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f8dbdd3719e534860d6a78526aafc220e0241f981367018c2875178cf83a413", size = 120573, upload-time = "2026-03-06T02:52:50.163Z" }, - { url = "https://files.pythonhosted.org/packages/47/c5/242cae3b5b080cd09bacef0591691ba1879739050cc7c801ff35c8886b66/wrapt-2.1.2-cp313-cp313-win32.whl", hash = "sha256:5c35b5d82b16a3bc6e0a04349b606a0582bc29f573786aebe98e0c159bc48db6", size = 58205, upload-time = "2026-03-06T02:53:47.494Z" }, - { url = "https://files.pythonhosted.org/packages/12/69/c358c61e7a50f290958809b3c61ebe8b3838ea3e070d7aac9814f95a0528/wrapt-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:f8bc1c264d8d1cf5b3560a87bbdd31131573eb25f9f9447bb6252b8d4c44a3a1", size = 60452, upload-time = "2026-03-06T02:53:30.038Z" }, - { url = "https://files.pythonhosted.org/packages/8e/66/c8a6fcfe321295fd8c0ab1bd685b5a01462a9b3aa2f597254462fc2bc975/wrapt-2.1.2-cp313-cp313-win_arm64.whl", hash = "sha256:3beb22f674550d5634642c645aba4c72a2c66fb185ae1aebe1e955fae5a13baf", size = 58842, upload-time = "2026-03-06T02:52:52.114Z" }, - { url = "https://files.pythonhosted.org/packages/da/55/9c7052c349106e0b3f17ae8db4b23a691a963c334de7f9dbd60f8f74a831/wrapt-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0fc04bc8664a8bc4c8e00b37b5355cffca2535209fba1abb09ae2b7c76ddf82b", size = 63075, upload-time = "2026-03-06T02:53:19.108Z" }, - { url = "https://files.pythonhosted.org/packages/09/a8/ce7b4006f7218248dd71b7b2b732d0710845a0e49213b18faef64811ffef/wrapt-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a9b9d50c9af998875a1482a038eb05755dfd6fe303a313f6a940bb53a83c3f18", size = 63719, upload-time = "2026-03-06T02:54:33.452Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e5/2ca472e80b9e2b7a17f106bb8f9df1db11e62101652ce210f66935c6af67/wrapt-2.1.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d3ff4f0024dd224290c0eabf0240f1bfc1f26363431505fb1b0283d3b08f11d", size = 152643, upload-time = "2026-03-06T02:52:42.721Z" }, - { url = "https://files.pythonhosted.org/packages/36/42/30f0f2cefca9d9cbf6835f544d825064570203c3e70aa873d8ae12e23791/wrapt-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3278c471f4468ad544a691b31bb856374fbdefb7fee1a152153e64019379f015", size = 158805, upload-time = "2026-03-06T02:54:25.441Z" }, - { url = "https://files.pythonhosted.org/packages/bb/67/d08672f801f604889dcf58f1a0b424fe3808860ede9e03affc1876b295af/wrapt-2.1.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8914c754d3134a3032601c6984db1c576e6abaf3fc68094bb8ab1379d75ff92", size = 145990, upload-time = "2026-03-06T02:53:57.456Z" }, - { url = "https://files.pythonhosted.org/packages/68/a7/fd371b02e73babec1de6ade596e8cd9691051058cfdadbfd62a5898f3295/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:ff95d4264e55839be37bafe1536db2ab2de19da6b65f9244f01f332b5286cfbf", size = 155670, upload-time = "2026-03-06T02:54:55.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/2d/9fe0095dfdb621009f40117dcebf41d7396c2c22dca6eac779f4c007b86c/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:76405518ca4e1b76fbb1b9f686cff93aebae03920cc55ceeec48ff9f719c5f67", size = 144357, upload-time = "2026-03-06T02:54:24.092Z" }, - { url = "https://files.pythonhosted.org/packages/0e/b6/ec7b4a254abbe4cde9fa15c5d2cca4518f6b07d0f1b77d4ee9655e30280e/wrapt-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c0be8b5a74c5824e9359b53e7e58bef71a729bacc82e16587db1c4ebc91f7c5a", size = 150269, upload-time = "2026-03-06T02:53:31.268Z" }, - { url = "https://files.pythonhosted.org/packages/6e/6b/2fabe8ebf148f4ee3c782aae86a795cc68ffe7d432ef550f234025ce0cfa/wrapt-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:f01277d9a5fc1862f26f7626da9cf443bebc0abd2f303f41c5e995b15887dabd", size = 59894, upload-time = "2026-03-06T02:54:15.391Z" }, - { url = "https://files.pythonhosted.org/packages/ca/fb/9ba66fc2dedc936de5f8073c0217b5d4484e966d87723415cc8262c5d9c2/wrapt-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:84ce8f1c2104d2f6daa912b1b5b039f331febfeee74f8042ad4e04992bd95c8f", size = 63197, upload-time = "2026-03-06T02:54:41.943Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1c/012d7423c95d0e337117723eb8ecf73c622ce15a97847e84cf3f8f26cd7e/wrapt-2.1.2-cp313-cp313t-win_arm64.whl", hash = "sha256:a93cd767e37faeddbe07d8fc4212d5cba660af59bdb0f6372c93faaa13e6e679", size = 60363, upload-time = "2026-03-06T02:54:48.093Z" }, - { url = "https://files.pythonhosted.org/packages/1a/c7/8528ac2dfa2c1e6708f647df7ae144ead13f0a31146f43c7264b4942bf12/wrapt-2.1.2-py3-none-any.whl", hash = "sha256:b8fd6fa2b2c4e7621808f8c62e8317f4aae56e59721ad933bac5239d913cf0e8", size = 43993, upload-time = "2026-03-06T02:53:12.905Z" }, + { url = "https://files.pythonhosted.org/packages/88/d1/a1b08f8f4fac8cbb156fa51cf64ee2c7f7f74f9875ba3cf70b3c58368694/wrapt-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d2beb1c7cab10603aecdc42f8edd6ff013f9a32e4543474e38e6b77ce9975aeb", size = 80831, upload-time = "2026-05-22T14:48:15.598Z" }, + { url = "https://files.pythonhosted.org/packages/54/ce/57890814991446a845e09b3445ce8b694f27eb0577004f2c2a36a9772ed4/wrapt-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0cb7e4dd71f4c32e5e84843cd3c4cd65dda034314004bbe1d7f99af2426ab80", size = 81375, upload-time = "2026-05-22T14:48:17.071Z" }, + { url = "https://files.pythonhosted.org/packages/38/65/08d7a6c76ac4493bdb668205ee9c1de1bd5daca61717c3e9aa49b4c01499/wrapt-2.2.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95821352042722cd9f1108874579a47989d0a7e12a37d87d2fc4af20fd99ab8a", size = 167417, upload-time = "2026-05-22T14:48:18.303Z" }, + { url = "https://files.pythonhosted.org/packages/62/ce/f1ccbee7a1bfe5cdc6b3da6bab4b45713d628b9294da32a39f563d648140/wrapt-2.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:abd621552ede77c4c69be7fac44ba911225b0c812b6ba604e5964cf98085b474", size = 166948, upload-time = "2026-05-22T14:48:19.768Z" }, + { url = "https://files.pythonhosted.org/packages/86/2a/f85d48d1cd4869aee6704028d257d740a47c1c467b457ce396b4b5b55d07/wrapt-2.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e3677c7146ce694874941ba82b57092cc4875445aadf29d72807351023105143", size = 158148, upload-time = "2026-05-22T14:48:21.96Z" }, + { url = "https://files.pythonhosted.org/packages/fe/5c/93939ad11d4a12358ab1aab219a2ef5efa5612e0db6b9fc65af8af1a891b/wrapt-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:9a5934eaea872e17936b5f45501eba5ab0bce9a74122e172b663d7c28c459c4a", size = 165905, upload-time = "2026-05-22T14:48:23.373Z" }, + { url = "https://files.pythonhosted.org/packages/e0/22/b8c2aa89862ff58605934d7abf4b70e6a5a1c33df96656f49035ccdf1c8a/wrapt-2.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f5b9daf6b629fce418e0cc3dd0436eac045188fa35deadb7a7f3941d5b8203f9", size = 156712, upload-time = "2026-05-22T14:48:24.767Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/bf00a7b02239c12bb02ddcc3c0b971bfcc36e578c5a44f1ccfef5b458545/wrapt-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f53ac9f3ef573326d009ed809beff4efcac6451931c2b8132586da4b9e53ff31", size = 166560, upload-time = "2026-05-22T14:48:26.83Z" }, + { url = "https://files.pythonhosted.org/packages/fe/93/6390ca9c5b787683cef588d04f57c8d41b9a2323b5597a65f18638c90ef2/wrapt-2.2.1-cp313-cp313-win32.whl", hash = "sha256:1ffa9cfd4bdb581539951b14ae661ff20ed0c3599b3e911a131ee0ec5ac11337", size = 77817, upload-time = "2026-05-22T14:48:28.221Z" }, + { url = "https://files.pythonhosted.org/packages/97/73/ce10f0e71c0cfaa1a65faadb8efd4852028b3bb9ba28932b8889df769d38/wrapt-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:368eac1e20fd0bb03dd3cc42bf9887154c3861b60989389ccb5fac032617d215", size = 80736, upload-time = "2026-05-22T14:48:30.139Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4c/89f4a6818fafbbd840330e4fa3873073e1bfc166133a64cac7f8fde7a5e3/wrapt-2.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:c754dafdf5aaf0b401b644a90a30046929a0dd1a536e0ff0ec959a59155d9c7f", size = 79099, upload-time = "2026-05-22T14:48:31.405Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f2/9a8741c46f8c208ac0a45b25ba170bcb4fb72a2781d5fb97dbd7b6be73cb/wrapt-2.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:ed928d0fda15fc0adc8d13305c8b3c0f2fba5b0669950c9e6d019d9162a3b3e8", size = 82802, upload-time = "2026-05-22T14:48:33.307Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0d/e9c855716a3705eef1416456bdf062b60620726fdc59428ff670fc3c60dc/wrapt-2.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fafb4e739e43544d12cb4abd1605fd4683b6ca6a9ad682b7fd8f4d21973eafa8", size = 83329, upload-time = "2026-05-22T14:48:34.593Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d6/a88f1c13112b7831adac75cea65d8310e0d696d570c8961844c90a57b865/wrapt-2.2.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:74d6a0c31472fe5d814917266b9f46495d7c61ed890af08b468acea92fb89a8d", size = 202937, upload-time = "2026-05-22T14:48:35.859Z" }, + { url = "https://files.pythonhosted.org/packages/42/65/e29d54aef06a4d898a5b8a25589a0b3769bde454f922fad8f6f89fbfb650/wrapt-2.2.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab5be648d5a0b86b7438864f8df3c705a65cef35a2fd3e5561e3e203167e0f27", size = 209997, upload-time = "2026-05-22T14:48:38.153Z" }, + { url = "https://files.pythonhosted.org/packages/2a/91/e4454263516cf0e12640912fbca9a83654e424f0a6ddb79f5cd7ce14bf33/wrapt-2.2.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d8f204c8e3a8bf9ece17e0a83d137fd807440977f8a5e762d59306795011440", size = 194856, upload-time = "2026-05-22T14:48:39.69Z" }, + { url = "https://files.pythonhosted.org/packages/de/d0/fe0ee202286afdf4a7f77dd29f195703145764d572aec209c5086e57d924/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d047f6498c973874ba08ac3f97c69a2c4b2211c8de6f4c205f75cb1c9522596e", size = 205654, upload-time = "2026-05-22T14:48:43.456Z" }, + { url = "https://files.pythonhosted.org/packages/23/b6/87d860dfc6460c246af70b1fd5c8b76df77571b42a493459423ded94fd7d/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:7a4fdb9326aab4a5a477a1640e5ad786a8495901009d7e7b038371edd23a9d2b", size = 192206, upload-time = "2026-05-22T14:48:44.858Z" }, + { url = "https://files.pythonhosted.org/packages/df/46/3eea8cde077d985f239a38c0257087b8064fd9ee9b1a99e282d2c86da4ef/wrapt-2.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c8cc5094b08abeae52da9c73c8a32003623be691a5193df2f4e3eac3d557c394", size = 198428, upload-time = "2026-05-22T14:48:46.319Z" }, + { url = "https://files.pythonhosted.org/packages/18/dc/b927ee9c7fc67adc3a5658f246a0d275425eb840ba36e7b702e70f18bde8/wrapt-2.2.1-cp313-cp313t-win32.whl", hash = "sha256:9907a4402ab6db12b7077a0ea5d7a4d028ecb22c8eee2b53527080d347cd1562", size = 79448, upload-time = "2026-05-22T14:48:47.901Z" }, + { url = "https://files.pythonhosted.org/packages/ec/b3/fd30b473fe498c70e6b9a5f328b8d3fbaf1b8c3c481465f59724bba8eb70/wrapt-2.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:5590d63f5243251641cf543009b4c9314a79d0598fdb8a8e4cfc918494536c53", size = 83021, upload-time = "2026-05-22T14:48:49.201Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f3/96c39153a8737a6e9aa85adef254ac4195bea3f2d24efc60472ccc3c9e2e/wrapt-2.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:c318a64b53d97b841d7b5e637517e50a27be64bc695128422953d4b21710954e", size = 80295, upload-time = "2026-05-22T14:48:50.479Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/29ac9daf11a86c22a8c38cd9236c62928ccae83f7ceb06bd3b0467cf9d05/wrapt-2.2.1-py3-none-any.whl", hash = "sha256:3aafea2975caef8ca49400640dde02cc7426e798f24870ed01f490bc3cffd32f", size = 61000, upload-time = "2026-05-22T14:49:41.593Z" }, +] + +[[package]] +name = "wxpython" +version = "4.2.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/43/81657a6b126ffc19163500a8184d683cec08eb4e1d06905cd0c371c702d0/wxpython-4.2.5.tar.gz", hash = "sha256:44e836d1bccd99c38790bb034b6ecf70d9060f6734320560f7c4b0d006144793", size = 58732217, upload-time = "2026-02-08T20:40:42.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/b9/1fee711ad5c26e7bbd4e10fae14b2ce0499684a084c3c60169373496ceae/wxpython-4.2.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f7ec6b028e8b1c4cad1ecb5c8402c2cae7840a25758be0fc209e56df86d1cac", size = 17775906, upload-time = "2026-02-08T20:40:12.031Z" }, + { url = "https://files.pythonhosted.org/packages/3d/53/521a79cbb169ab6b123e79ea23ebafe3046c1999a431b6fc864f1217bb86/wxpython-4.2.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:77ac5335d8e4aae92732fc039df24a58181cdfb5bc7931692f1f9415e9eeee7d", size = 18857767, upload-time = "2026-02-08T20:40:14.486Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ea/a69ad0a1e7b01876619982b6cf8db0e26d0f3776b9be73b61d9f0662a2ce/wxpython-4.2.5-cp313-cp313-win32.whl", hash = "sha256:0985f190565b94635f146989886196a7e9faced8911800910460919cb72668cc", size = 14520419, upload-time = "2026-02-08T20:40:17.401Z" }, + { url = "https://files.pythonhosted.org/packages/a8/bd/d2698369dbc43aa5c9324c23fdd5cd3b23c245861e334b1d976209913f90/wxpython-4.2.5-cp313-cp313-win_amd64.whl", hash = "sha256:3fd3649fc4752f1a02776b7057073c932e5229bbab2031762b01532bcc6bd074", size = 16576741, upload-time = "2026-02-08T20:40:20.646Z" }, + { url = "https://files.pythonhosted.org/packages/cb/4e/4181734a2bc05940ba4feb3feb2474416b1dc12c329a2ac164632582c4d6/wxpython-4.2.5-cp313-cp313-win_arm64.whl", hash = "sha256:b794d9912464990ea1fd3744fb73fbd7446149e230e5a611ba40eb4ac74755a1", size = 15537287, upload-time = "2026-02-08T20:40:24.658Z" }, ] From 8e45808366f601fcb0652327919ec4b4f11b34b2 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 25 Jun 2026 08:24:23 +0000 Subject: [PATCH 2/3] Pre-commit auto-fix --- addon/doc/uk/readme.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/addon/doc/uk/readme.md b/addon/doc/uk/readme.md index 4622c51..c2bcd6c 100644 --- a/addon/doc/uk/readme.md +++ b/addon/doc/uk/readme.md @@ -45,7 +45,7 @@ «Показати ключ API» -Якщо позначити прапорець, символи API-ключа стануть видимими. +Якщо позначити прапорець, символи API-ключа стануть видимими. Початково вони приховані з міркувань безпеки. ## Параметри вибору моделі @@ -136,7 +136,7 @@ Ctrl + Alt + A ## Непризначені сценарії -Нижченаведені сценарії не мають жестів. +Нижченаведені сценарії не мають жестів. Ви можете призначити їх у: Параметри → Жести вводу → Запитати OpenRouter @@ -193,4 +193,4 @@ https://openrouter.ai/settings/privacy * Цей додаток сумісний із версіями NVDA починаючи з 2025.1 і вище. -* Переклад: Георгій Галас. \ No newline at end of file +* Переклад: Георгій Галас. From a26e97b3d6e662226814647443fe572b0c2510ee Mon Sep 17 00:00:00 2001 From: Abdel792 Date: Thu, 25 Jun 2026 11:26:34 +0200 Subject: [PATCH 3/3] ci: install libgtk-3-dev dependency on github runner to fix wxpython build --- .github/workflows/build_addon.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build_addon.yml b/.github/workflows/build_addon.yml index 237a496..ec1cc73 100644 --- a/.github/workflows/build_addon.yml +++ b/.github/workflows/build_addon.yml @@ -30,7 +30,7 @@ jobs: - name: Install dependencies run: | sudo apt-get update -y - sudo apt-get install -y gettext + sudo apt-get install -y gettext libgtk-3-dev uv sync - name: Code checks