diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 34f7290d..fec763a1 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -34,8 +34,10 @@ jobs: uv sync pnpm install - - name: Check Python formatting (black) - run: uv run black --check webapp/ tests/ + - name: Check Python formatting & linting (ruff) + run: | + uv run ruff check webapp/ tests/ + uv run ruff format --check webapp/ tests/ - name: Check TypeScript formatting (prettier) run: pnpm format:check diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..95c9fafa --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,15 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.6 + hooks: + - id: ruff + args: [--fix] + - id: ruff-format + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.5.3 + hooks: + - id: prettier + types_or: [javascript, ts, json, vue] + additional_dependencies: + - prettier@3.8.1 diff --git a/CLAUDE.md b/CLAUDE.md index 860f24e6..0591f541 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -77,10 +77,11 @@ pnpm dev # Starts both Flask server + Vite watcher ### Before Committing ```bash -pnpm format # Format TypeScript -uv run black webapp/ tests/ # Format Python -pnpm test # Run TS tests -uv run pytest tests/ # Run Python tests +pnpm format # Format TypeScript +uv run ruff format webapp/ tests/ # Format Python +uv run ruff check webapp/ tests/ # Lint Python +pnpm test # Run TS tests +uv run pytest tests/ # Run Python tests ``` ## Important Notes @@ -107,8 +108,9 @@ These are tracked by pytest but not blocking - they're data issues, not code iss ### Python -- Black formatter, 100 char line length -- Run `uv run black webapp/ tests/` before committing +- Ruff formatter + linter, 100 char line length +- Run `uv run ruff format webapp/ tests/` and `uv run ruff check webapp/ tests/` before committing +- Pre-commit hooks run both automatically ## Don't diff --git a/pyproject.toml b/pyproject.toml index e7360a0d..66e047b4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,6 @@ dependencies = [ "flask>=3.1.0", "flask-cors>=6.0.0", "gunicorn>=24.0.0", - "Flask-FlatPages>=0.9.0", "openai>=2.21.0", "pillow>=12.1.1", ] @@ -15,20 +14,36 @@ dependencies = [ [dependency-groups] dev = [ "pytest>=8.0.0", - "black>=24.0.0", + "ruff>=0.11.0", "wordfreq>=3.1.1", ] -[tool.black] +[tool.ruff] line-length = 100 -target-version = ['py314'] -include = '\.pyi?$' -extend-exclude = ''' -/( - \.git - | \.venv - | venv - | node_modules - | webapp/static/dist -)/ -''' +target-version = "py311" +exclude = [ + ".git", + ".venv", + "venv", + "node_modules", + "webapp/static/dist", + "webapp/deprecated", + "tests/deprecated", +] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle errors + "W", # pycodestyle warnings + "F", # pyflakes + "I", # isort + "UP", # pyupgrade + "B", # flake8-bugbear + "SIM", # flake8-simplify +] +ignore = [ + "E501", # line too long (formatter handles this) +] + +[tool.ruff.lint.isort] +known-first-party = ["webapp"] diff --git a/scripts/analyze_word_quality.py b/scripts/analyze_word_quality.py index 6c5a21b2..d34b55cd 100644 --- a/scripts/analyze_word_quality.py +++ b/scripts/analyze_word_quality.py @@ -22,7 +22,6 @@ import argparse import sys from collections import defaultdict -from math import log from pathlib import Path SCRIPT_DIR = Path(__file__).parent @@ -133,7 +132,7 @@ def cmd_char_freq(args): for char, freq in sorted_chars: count = int(freq * len(words)) bar = "#" * int(freq * 100) - print(f" {char} {count:>6} {freq*100:>7.1f}% {bar}") + print(f" {char} {count:>6} {freq * 100:>7.1f}% {bar}") # Threshold analysis print(f"\n{'Threshold analysis':}") @@ -144,7 +143,7 @@ def cmd_char_freq(args): filtered = [w for w in words if any(c in rare_chars for c in w)] remaining = len(words) - len(filtered) print( - f" {threshold*100:>4.0f}% {len(rare_chars):>6} {len(filtered):>8} {remaining:>6}" + f" {threshold * 100:>4.0f}% {len(rare_chars):>6} {len(filtered):>8} {remaining:>6}" ) @@ -181,7 +180,7 @@ def cmd_difficult_words(args): # Filter by threshold if specified if threshold is not None: scored = [(w, f, c) for w, f, c in scored if f < threshold] - print(f"Words in {lang} daily list with rarest character below {threshold*100:.0f}%:") + print(f"Words in {lang} daily list with rarest character below {threshold * 100:.0f}%:") else: print(f"All words in {lang} daily list sorted by difficulty (hardest first):") @@ -191,7 +190,7 @@ def cmd_difficult_words(args): print(f"{'Word':<12} {'Rarest Char':>12} {'Char Freq %':>12}") print("-" * 38) for word, freq, char in scored: - print(f" {word:<10} {char:>8} {freq*100:>7.1f}%") + print(f" {word:<10} {char:>8} {freq * 100:>7.1f}%") print(f"\nTotal: {len(scored)} words") @@ -267,10 +266,10 @@ def cmd_hebrew_suffixes(args): print(f" → Keep: {keep}, blocklist: {to_block}") print() - print(f"{'='*50}") + print(f"{'=' * 50}") print(f"Total groups: {len(groups)}") print(f"Total words to blocklist: {total_to_blocklist}") - print(f"\nBlocklist additions (copy-paste ready):") + print("\nBlocklist additions (copy-paste ready):") for w in sorted(blocklist_words): print(w) @@ -296,7 +295,7 @@ def cmd_hebrew_quality(args): sys.exit(1) print(f"Hebrew daily word quality analysis ({len(words)} words)") - print(f"Cross-referencing with wordfreq (Wikipedia, Reddit, Google Books, etc.)\n") + print("Cross-referencing with wordfreq (Wikipedia, Reddit, Google Books, etc.)\n") # Score each word not_in_wordfreq = [] @@ -313,16 +312,16 @@ def cmd_hebrew_quality(args): normal.append((word, zf)) # Report - print(f"Category breakdown:") + print("Category breakdown:") print(f" Normal (zipf >= 2.0): {len(normal):>5} words") print(f" Low frequency (zipf < 2.0): {len(low_wordfreq):>5} words") print(f" Not in wordfreq at all: {len(not_in_wordfreq):>5} words") if not_in_wordfreq: - print(f"\n{'='*50}") + print(f"\n{'=' * 50}") print(f"Words NOT found in wordfreq ({len(not_in_wordfreq)} words)") - print(f"These may be proper nouns, obscure, or malformed:") - print(f"{'='*50}") + print("These may be proper nouns, obscure, or malformed:") + print(f"{'=' * 50}") # Show first N limit = args.limit or 100 for word, zf in sorted(not_in_wordfreq)[:limit]: @@ -331,10 +330,10 @@ def cmd_hebrew_quality(args): print(f" ... and {len(not_in_wordfreq) - limit} more") if low_wordfreq: - print(f"\n{'='*50}") + print(f"\n{'=' * 50}") print(f"Low-frequency words (zipf < 2.0, {len(low_wordfreq)} words)") - print(f"These may be uncommon or domain-specific:") - print(f"{'='*50}") + print("These may be uncommon or domain-specific:") + print(f"{'=' * 50}") low_wordfreq.sort(key=lambda x: x[1]) limit = args.limit or 50 for word, zf in low_wordfreq[:limit]: @@ -348,7 +347,7 @@ def cmd_hebrew_quality(args): if has_freq: avg = sum(has_freq) / len(has_freq) print( - f"\nWordfreq coverage: {len(has_freq)}/{len(words)} words ({100*len(has_freq)/len(words):.1f}%)" + f"\nWordfreq coverage: {len(has_freq)}/{len(words)} words ({100 * len(has_freq) / len(words):.1f}%)" ) print(f"Average zipf frequency (of found words): {avg:.2f}") diff --git a/scripts/configs.ipynb b/scripts/configs.ipynb index 2aa67a19..a9f68a1c 100644 --- a/scripts/configs.ipynb +++ b/scripts/configs.ipynb @@ -48,7 +48,7 @@ "data_dir = \"../webapp/data/\"\n", "language_codes = [f.split(\"/\")[-1] for f in glob.glob(f\"{data_dir}/languages/*\")]\n", "\n", - "with open(f\"{data_dir}/languages.json\", \"r\") as f:\n", + "with open(f\"{data_dir}/languages.json\") as f:\n", " languages = json.load(f)\n", "\n", "print(languages[\"en\"])" @@ -107,7 +107,7 @@ ], "source": [ "# load english language config\n", - "with open(f\"{data_dir}/languages/en/language_config.json\", \"r\") as f:\n", + "with open(f\"{data_dir}/languages/en/language_config.json\") as f:\n", " en_config = json.load(f)\n", "\n", "f\"{data_dir}/languages/en/language_config.json\"" @@ -131,7 +131,6 @@ " # load from_language config\n", " with open(\n", " f\"{data_dir}languages/{from_language}/language_config.json\",\n", - " \"r\",\n", " encoding=\"utf-8\",\n", " ) as f:\n", " from_language_config = json.load(f)\n", @@ -141,7 +140,6 @@ " if os.path.exists(f\"{data_dir}languages/{to_language}/language_config.json\"):\n", " with open(\n", " f\"{data_dir}languages/{to_language}/language_config.json\",\n", - " \"r\",\n", " encoding=\"utf-8\",\n", " ) as f:\n", " to_language_config = json.load(f)\n", @@ -158,9 +156,7 @@ " language_config[\"meta\"] = {}\n", " language_config[\"meta\"][\"locale\"] = to_language\n", " language_config[\"text\"] = {}\n", - " language_config[\"text\"][\"subheader\"] = languages[to_language][\n", - " \"language_name_native\"\n", - " ]\n", + " language_config[\"text\"][\"subheader\"] = languages[to_language][\"language_name_native\"]\n", "\n", " # defaults\n", " language_config[\"language_code_3\"] = \"\"\n", @@ -202,9 +198,7 @@ "\n", " language_config[\"help\"] = {}\n", " for key in from_language_config[\"help\"]:\n", - " translated_text = translate_text(\n", - " from_language_config[\"help\"][key], to_language\n", - " )\n", + " translated_text = translate_text(from_language_config[\"help\"][key], to_language)\n", " if key in [\n", " \"text_2_1\",\n", " \"text_2_2\",\n", @@ -553,14 +547,13 @@ "\n", "import glob\n", "import json\n", - "import os\n", "\n", "data_dir = \"../webapp/data/\"\n", "\n", "for lang in glob.glob(f\"{data_dir}languages/*\"):\n", " lang = lang.split(\"/\")[-1]\n", " try:\n", - " with open(f\"{data_dir}/languages/{lang}/language_config.json\", \"r\") as f:\n", + " with open(f\"{data_dir}/languages/{lang}/language_config.json\") as f:\n", " language_config = json.load(f)\n", "\n", " with open(f\"{data_dir}/languages/{lang}/keyboard.json\", \"w\") as f:\n", @@ -601,7 +594,7 @@ "for lang in glob.glob(f\"{data_dir}languages/*\"):\n", " lang = lang.split(\"/\")[-1]\n", " try:\n", - " with open(f\"{data_dir}/languages/{lang}/{lang}_keyboard.json\", \"r\") as f:\n", + " with open(f\"{data_dir}/languages/{lang}/{lang}_keyboard.json\") as f:\n", " keyboard = json.load(f)\n", "\n", " keyboard = []\n", diff --git a/scripts/curate_words.py b/scripts/curate_words.py index b7781746..3e096845 100755 --- a/scripts/curate_words.py +++ b/scripts/curate_words.py @@ -20,8 +20,6 @@ import argparse import datetime -import os -import shutil from pathlib import Path # Paths @@ -79,7 +77,7 @@ def extract_next_words(lang: str, num_days: int = 365) -> None: f.write(f"{start_idx + i}: {word}\n") print(f"Wrote {num_days} words to {output_file}") - print(f"Review the file and identify words to remove.") + print("Review the file and identify words to remove.") def remove_words(lang: str, words_to_remove: list[str]) -> None: diff --git a/scripts/deprecated/capture_wiktionary_fixtures.py b/scripts/deprecated/capture_wiktionary_fixtures.py index ffa6dbf6..64eab0ea 100644 --- a/scripts/deprecated/capture_wiktionary_fixtures.py +++ b/scripts/deprecated/capture_wiktionary_fixtures.py @@ -10,7 +10,6 @@ """ import json -import os import sys import time import urllib.parse @@ -21,7 +20,7 @@ PROJECT_ROOT = Path(__file__).parent.parent sys.path.insert(0, str(PROJECT_ROOT / "webapp")) -from wiktionary import parse_wikt_definition, WIKT_LANG_MAP +from wiktionary import WIKT_LANG_MAP, parse_wikt_definition LANGUAGES_DIR = PROJECT_ROOT / "webapp" / "data" / "languages" FIXTURES_DIR = PROJECT_ROOT / "tests" / "fixtures" / "wiktionary" @@ -32,7 +31,7 @@ def load_word_list(lang_code): word_file = LANGUAGES_DIR / lang_code / f"{lang_code}_5words.txt" if not word_file.exists(): return [] - with open(word_file, "r", encoding="utf-8") as f: + with open(word_file, encoding="utf-8") as f: return [line.strip() for line in f if line.strip()] diff --git a/scripts/hunspellToJSON.py b/scripts/hunspellToJSON.py index b83e34d5..f449cc66 100644 --- a/scripts/hunspellToJSON.py +++ b/scripts/hunspellToJSON.py @@ -1,5 +1,9 @@ #!/usr/bin/python3.3 -import re, argparse, os, gzip, json +import argparse +import gzip +import json +import os +import re def file_to_list(in_file): @@ -393,15 +397,15 @@ def main(): # Open AFF file try: - aff_file = open(aff_path, "r", encoding="ISO8859-1") + aff_file = open(aff_path, encoding="ISO8859-1") aff_rules = AFF(aff_file) aff_file.close() - except IOError: + except OSError: print(aff_path + " not found") # Open DIC file try: - dict_file = open(dict_path, "r", encoding="ISO8859-1") + dict_file = open(dict_path, encoding="ISO8859-1") dictionary = DICT( dict_file, aff_rules, @@ -414,7 +418,6 @@ def main(): # Open output file if args.output: - if args.gzip: out_file = gzip.open(args.output, "wb") else: @@ -433,7 +436,7 @@ def main(): out_file.close() dict_file.close() - except IOError: + except OSError: print(dict_path + " not found") diff --git a/scripts/improve_word_lists.py b/scripts/improve_word_lists.py index d5a00ca2..06b4154c 100644 --- a/scripts/improve_word_lists.py +++ b/scripts/improve_word_lists.py @@ -358,9 +358,9 @@ def process_language( result["reason"] = f"excluded ({lang})" return result - print(f"\n{'='*60}") + print(f"\n{'=' * 60}") print(f"Processing: {lang} ({LANG_NAMES.get(lang, lang)})") - print(f"{'='*60}") + print(f"{'=' * 60}") # Load existing data existing_words = load_word_list(lang) @@ -463,7 +463,7 @@ def process_language( daily_words = [w for w in daily_words if not any(c in rare_chars for c in w)] removed = pre_filter - len(daily_words) print( - f" Character difficulty filter ({threshold*100:.0f}%): " + f" Character difficulty filter ({threshold * 100:.0f}%): " f"removed {removed} words with rare chars {rare_chars}" ) @@ -728,9 +728,9 @@ def batch_process(daily_count: int, dry_run: bool, overwrite: bool): results.append(result) # Summary table - print(f"\n{'='*80}") + print(f"\n{'=' * 80}") print(f"{'SUMMARY':^80}") - print(f"{'='*80}") + print(f"{'=' * 80}") print( f"{'Lang':<6} {'Status':<12} {'Daily':<8} {'Supplement':<12} {'Freq Match':<12} {'Notes'}" ) diff --git a/scripts/languages.ipynb b/scripts/languages.ipynb index 84fd86b4..1163e200 100644 --- a/scripts/languages.ipynb +++ b/scripts/languages.ipynb @@ -28,7 +28,6 @@ "# for every folder in raw/ (except en), create a folder in webapp/data/\n", "\n", "import os\n", - "import glob\n", "import random\n", "\n", "# problematic: tlh, vi, sr, ne, el, rw\n", @@ -39,7 +38,7 @@ " filters a word dictionary to 5-letter words and saves them to a file\n", " \"\"\"\n", " words = []\n", - " with open(raw_dir + lang + \"/index.dic\", \"r\") as f:\n", + " with open(raw_dir + lang + \"/index.dic\") as f:\n", " i = 0\n", " for line in f:\n", " i += 1\n", @@ -60,7 +59,7 @@ "\n", " # if '_characters.txt' file exists in out_dir, load it and filter by it\n", " try:\n", - " with open(out_dir + lang + \"/\" + lang + \"_characters.txt\", \"r\") as f:\n", + " with open(out_dir + lang + \"/\" + lang + \"_characters.txt\") as f:\n", " characters = [line.strip() for line in f]\n", " except FileNotFoundError:\n", " characters = []\n", @@ -391,9 +390,7 @@ "# Custom arabic word source\n", "import pandas as pd\n", "\n", - "df = pd.read_excel(\n", - " raw_dir + \"ar/Top-50000-Arabic-Words-Masterlist_ModernStandardArabic.com_.xlsx\"\n", - ")" + "df = pd.read_excel(raw_dir + \"ar/Top-50000-Arabic-Words-Masterlist_ModernStandardArabic.com_.xlsx\")" ] }, { @@ -451,7 +448,6 @@ "outputs": [], "source": [ "# Let's do a more reusable version of all this\n", - "import random\n", "\n", "\n", "def process_wordlist(\n", @@ -501,12 +497,7 @@ " f.write(word + \"\\n\")\n", "\n", " with open(\n", - " out_dir\n", - " + \"languages/\"\n", - " + language_code\n", - " + \"/\"\n", - " + language_code\n", - " + \"_characters.txt\",\n", + " out_dir + \"languages/\" + language_code + \"/\" + language_code + \"_characters.txt\",\n", " \"w\",\n", " ) as f:\n", " for char in characters:\n", @@ -634,20 +625,16 @@ "# let's try a better arabic wordlist\n", "# using file: '../raw/ar/hans-wehr.wordlist.txt' and acceptable characters: '../webapp/data/languages/ar/ar_characters.txt'\n", "\n", - "with open(\"../raw/ar/hans-wehr.wordlist.txt\", \"r\") as f:\n", + "with open(\"../raw/ar/hans-wehr.wordlist.txt\") as f:\n", " words = f.read().split(\"\\n\")\n", "\n", - "with open(out_dir + \"ar/ar_characters.txt\", \"r\") as f:\n", + "with open(out_dir + \"ar/ar_characters.txt\") as f:\n", " characters = f.read().split(\"\\n\")\n", "\n", "print(f\"Arabic 5 words: {len(words)}\")\n", "words_clean = []\n", "for word in words:\n", - " if (\n", - " len(word) == 5\n", - " and word not in words_clean\n", - " and all(char in characters for char in word)\n", - " ):\n", + " if len(word) == 5 and word not in words_clean and all(char in characters for char in word):\n", " words_clean.append(word)\n", "\n", "print(len(words_clean))\n", @@ -924,7 +911,7 @@ "# load '../webapp/data/other_wordles.json'\n", "import json\n", "\n", - "with open(\"../webapp/data/other_wordles.json\", \"r\") as f:\n", + "with open(\"../webapp/data/other_wordles.json\") as f:\n", " other_wordles = json.load(f)\n", "\n", "# append wordles to '../webapp/data/other_wordles.json'\n", diff --git a/scripts/languages_characters.ipynb b/scripts/languages_characters.ipynb index f0d92e7d..8f0cb99e 100644 --- a/scripts/languages_characters.ipynb +++ b/scripts/languages_characters.ipynb @@ -140,11 +140,11 @@ } ], "source": [ - "from bs4 import BeautifulSoup\n", - "import requests\n", - "import json\n", - "import re\n", "import glob\n", + "import json\n", + "\n", + "import requests\n", + "from bs4 import BeautifulSoup\n", "\n", "base_url = \"https://unicode-org.github.io/cldr-staging/charts/latest/summary/\"\n", "\n", @@ -161,13 +161,9 @@ " soup = BeautifulSoup(response.text, \"html.parser\")\n", "\n", " # get the table\n", - " table = soup.find(\n", - " \"table\", {\"border\": \"1\", \"cellpadding\": \"2\", \"cellspacing\": \"0\"}\n", - " )\n", + " table = soup.find(\"table\", {\"border\": \"1\", \"cellpadding\": \"2\", \"cellspacing\": \"0\"})\n", " # find index of table header with text \"Native\"\n", - " header_index = [\n", - " i for i, th in enumerate(table.find_all(\"th\")) if \"Native\" in th.text\n", - " ][0]\n", + " header_index = [i for i, th in enumerate(table.find_all(\"th\")) if \"Native\" in th.text][0]\n", " text = table.find_all(\"td\")[header_index].text\n", "\n", " # remove [ and ]\n", @@ -189,7 +185,7 @@ "\n", "\n", "# store to json in /out/\n", - "with open(f\"out/languages_characters.json\", \"w\", encoding=\"utf-8\") as f:\n", + "with open(\"out/languages_characters.json\", \"w\", encoding=\"utf-8\") as f:\n", " json.dump(language_characters, f, ensure_ascii=False, indent=4)" ] }, diff --git a/scripts/playground.ipynb b/scripts/playground.ipynb index 1c09ddc0..d4a43cdb 100644 --- a/scripts/playground.ipynb +++ b/scripts/playground.ipynb @@ -485,9 +485,7 @@ " # column '639-1' is the language code\n", " language_code = row[1][\"639-1\"]\n", " language_name = row[1][\"ISO language name\"].split(\",\")[0].split(\"(\")[0].strip()\n", - " language_name_native = (\n", - " row[1][\"Native name (endonym)\"].split(\",\")[0].split(\"(\")[0].strip()\n", - " )\n", + " language_name_native = row[1][\"Native name (endonym)\"].split(\",\")[0].split(\"(\")[0].strip()\n", " language_emoji = ascii2lang[language_code[0]] + ascii2lang[language_code[1]]\n", "\n", " languages[language_code] = {\n", @@ -529,7 +527,7 @@ "# load arabic.json\n", "import json\n", "\n", - "with open(\"arabic.json\", \"r\") as f:\n", + "with open(\"arabic.json\") as f:\n", " arabic = json.load(f)\n", "arabic" ] diff --git a/scripts/solver.ipynb b/scripts/solver.ipynb index cec42a4e..17825509 100644 --- a/scripts/solver.ipynb +++ b/scripts/solver.ipynb @@ -1084,7 +1084,7 @@ "bad_letters = [c for c in \"rtuiopsdfghcnl\"]\n", "for word in five_letter_words:\n", " if not any(letter in word for letter in bad_letters):\n", - " if \"a\" == word[0] and \"e\" == word[3]:\n", + " if word[0] == \"a\" and word[3] == \"e\":\n", " second_list.append(word)\n", "print(len(second_list))" ] diff --git a/scripts/status.ipynb b/scripts/status.ipynb index 2deffeb9..5ed79062 100644 --- a/scripts/status.ipynb +++ b/scripts/status.ipynb @@ -82,6 +82,7 @@ "\n", "import glob\n", "import json\n", + "\n", "import matplotlib.pyplot as plt\n", "\n", "data_dir = \"../webapp/data/\"\n", @@ -157,7 +158,7 @@ " languages = {}\n", " # for each language folder, get the language config.name and config.name_natove\n", " for lang in language_codes:\n", - " with open(f\"{data_dir}languages/{lang}/language_config.json\", \"r\") as f:\n", + " with open(f\"{data_dir}languages/{lang}/language_config.json\") as f:\n", " language_config = json.load(f)\n", " languages[lang] = {\n", " \"language_name\": language_config[\"name\"],\n", @@ -172,7 +173,7 @@ "\n", "def load_supplemental_words(lang):\n", " try:\n", - " with open(f\"{data_dir}languages/{lang}/{lang}_5words_supplement.txt\", \"r\") as f:\n", + " with open(f\"{data_dir}languages/{lang}/{lang}_5words_supplement.txt\") as f:\n", " supplemental_words = [line.strip() for line in f]\n", " except FileNotFoundError:\n", " supplemental_words = []\n", @@ -201,7 +202,7 @@ "\n", " language_name = languages[code][\"language_name\"]\n", " f.write(\n", - " f\" - {language_name} ({code}): {' '*(25-(len(language_name)+len(code)))} {status_emoji} ({len(words) + len(words_supplement)} words)\\n\"\n", + " f\" - {language_name} ({code}): {' ' * (25 - (len(language_name) + len(code)))} {status_emoji} ({len(words) + len(words_supplement)} words)\\n\"\n", " )" ] }, diff --git a/scripts/utils/google_trans_new.py b/scripts/utils/google_trans_new.py index f4f59cfa..0b5202ad 100644 --- a/scripts/utils/google_trans_new.py +++ b/scripts/utils/google_trans_new.py @@ -1,13 +1,17 @@ -# coding:utf-8 # author LuShan # from Hugo: thanks LuShan! # version : 1.1.9 -import json, requests, random, re +import json +import logging +import random +import re from urllib.parse import quote + +import requests import urllib3 -import logging -from .constant import LANGUAGES, DEFAULT_SERVICE_URLS + +from .constant import DEFAULT_SERVICE_URLS, LANGUAGES log = logging.getLogger(__name__) log.addHandler(logging.NullHandler()) @@ -32,7 +36,7 @@ def __init__(self, msg=None, **kwargs): self.msg = self.infer_msg(self.tts, self.rsp) else: self.msg = None - super(google_new_transError, self).__init__(self.msg) + super().__init__(self.msg) def infer_msg(self, tts, rsp=None): cause = "Unknown" @@ -49,7 +53,7 @@ def infer_msg(self, tts, rsp=None): status = rsp.status_code reason = rsp.reason - premise = "{:d} ({}) from TTS API".format(status, reason) + premise = f"{status:d} ({reason}) from TTS API" if status == 403: cause = "Bad token or upstream API changes" @@ -58,7 +62,7 @@ def infer_msg(self, tts, rsp=None): elif status >= 500: cause = "Uptream API error. Try again later." - return "{}. Probable cause: {}".format(premise, cause) + return f"{premise}. Probable cause: {cause}" class google_translator: @@ -97,7 +101,7 @@ def __init__(self, url_suffix="cn", timeout=5, proxies=None): self.url_suffix = URL_SUFFIX_DEFAULT else: self.url_suffix = url_suffix - url_base = "https://translate.google.{}".format(self.url_suffix) + url_base = f"https://translate.google.{self.url_suffix}" self.url = url_base + "/_/TranslateWebserverUi/data/batchexecute" self.timeout = timeout @@ -108,7 +112,7 @@ def _package_rpc(self, text, lang_src="auto", lang_tgt="auto"): rpc = [[[random.choice(GOOGLE_TTS_RPC), escaped_parameter, None, "generic"]]] espaced_rpc = json.dumps(rpc, separators=(",", ":")) # text_urldecode = quote(text.strip()) - freq_initial = "f.req={}&".format(quote(espaced_rpc)) + freq_initial = f"f.req={quote(espaced_rpc)}&" freq = freq_initial return freq @@ -127,7 +131,7 @@ def translate(self, text, lang_tgt="auto", lang_src="auto", pronounce=False): if len(text) == 0: return "" headers = { - "Referer": "http://translate.google.{}/".format(self.url_suffix), + "Referer": f"http://translate.google.{self.url_suffix}/", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/47.0.2526.106 Safari/537.36", @@ -192,10 +196,10 @@ def translate(self, text, lang_tgt="auto", lang_src="auto", pronounce=False): r.raise_for_status() except requests.exceptions.ConnectTimeout as e: raise e - except requests.exceptions.HTTPError as e: + except requests.exceptions.HTTPError: # Request successful, bad response raise google_new_transError(tts=self, response=r) - except requests.exceptions.RequestException as e: + except requests.exceptions.RequestException: # Request failed raise google_new_transError(tts=self) @@ -206,7 +210,7 @@ def detect(self, text): if len(text) == 0: return "" headers = { - "Referer": "http://translate.google.{}/".format(self.url_suffix), + "Referer": f"http://translate.google.{self.url_suffix}/", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/47.0.2526.106 Safari/537.36", diff --git a/tests/conftest.py b/tests/conftest.py index 76230b32..71f1fc08 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,11 +2,11 @@ Pytest configuration and shared fixtures for Wordle Global tests. """ -import pytest -import os import json from pathlib import Path +import pytest + # Exclude deprecated tests from collection collect_ignore_glob = ["deprecated/*"] @@ -62,7 +62,7 @@ def load_word_list(lang_code: str) -> list[str]: word_file = LANGUAGES_DIR / lang_code / f"{lang_code}_5words.txt" if not word_file.exists(): return [] - with open(word_file, "r", encoding="utf-8") as f: + with open(word_file, encoding="utf-8") as f: return [line.strip() for line in f if line.strip()] @@ -71,7 +71,7 @@ def load_supplement_words(lang_code: str) -> list[str]: word_file = LANGUAGES_DIR / lang_code / f"{lang_code}_5words_supplement.txt" if not word_file.exists(): return [] - with open(word_file, "r", encoding="utf-8") as f: + with open(word_file, encoding="utf-8") as f: return [line.strip() for line in f if line.strip()] @@ -83,7 +83,7 @@ def load_daily_words(lang_code: str) -> list[str]: word_file = LANGUAGES_DIR / lang_code / f"{lang_code}_daily_words.txt" if not word_file.exists(): return [] - with open(word_file, "r", encoding="utf-8") as f: + with open(word_file, encoding="utf-8") as f: return [ line.strip().lower() for line in f if line.strip() and not line.strip().startswith("#") ] @@ -94,7 +94,7 @@ def load_blocklist(lang_code: str) -> set[str]: blocklist_file = LANGUAGES_DIR / lang_code / f"{lang_code}_blocklist.txt" if not blocklist_file.exists(): return set() - with open(blocklist_file, "r", encoding="utf-8") as f: + with open(blocklist_file, encoding="utf-8") as f: return { line.strip().lower() for line in f if line.strip() and not line.strip().startswith("#") } @@ -105,7 +105,7 @@ def load_characters(lang_code: str) -> list[str]: char_file = LANGUAGES_DIR / lang_code / f"{lang_code}_characters.txt" if not char_file.exists(): return [] - with open(char_file, "r", encoding="utf-8") as f: + with open(char_file, encoding="utf-8") as f: return [line.strip() for line in f if line.strip()] @@ -114,7 +114,7 @@ def load_language_config(lang_code: str) -> dict | None: config_file = LANGUAGES_DIR / lang_code / "language_config.json" if not config_file.exists(): return None - with open(config_file, "r", encoding="utf-8") as f: + with open(config_file, encoding="utf-8") as f: return json.load(f) @@ -144,7 +144,7 @@ def load_keyboard(lang_code: str) -> list | None: keyboard_file = LANGUAGES_DIR / lang_code / f"{lang_code}_keyboard.json" if not keyboard_file.exists(): return None - with open(keyboard_file, "r", encoding="utf-8") as f: + with open(keyboard_file, encoding="utf-8") as f: data = json.load(f) # New multi-layout format diff --git a/tests/deprecated/test_wiktionary.py b/tests/deprecated/test_wiktionary.py index 55b9eef4..993d64ef 100644 --- a/tests/deprecated/test_wiktionary.py +++ b/tests/deprecated/test_wiktionary.py @@ -8,11 +8,10 @@ pytest tests/test_wiktionary.py --run-network # all tests """ -import io import json import sys from pathlib import Path -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import pytest @@ -20,17 +19,14 @@ sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "webapp")) from wiktionary import ( - parse_wikt_definition, - _fallback_extract_definition, _build_candidates, + _fallback_extract_definition, _follow_form_of, - fetch_native_wiktionary, + fetch_definition, fetch_english_definition, fetch_llm_definition, - fetch_definition, - strip_html, - LEMMA_STRIP_RULES, - LEMMA_SUFFIXES, + fetch_native_wiktionary, + parse_wikt_definition, ) # --------------------------------------------------------------------------- diff --git a/tests/deprecated/test_wiktionary_definitions.py b/tests/deprecated/test_wiktionary_definitions.py index a35bb648..045d2ada 100644 --- a/tests/deprecated/test_wiktionary_definitions.py +++ b/tests/deprecated/test_wiktionary_definitions.py @@ -30,7 +30,7 @@ def load_word_list(lang_code): word_file = LANGUAGES_DIR / lang_code / f"{lang_code}_5words.txt" if not word_file.exists(): return [] - with open(word_file, "r", encoding="utf-8") as f: + with open(word_file, encoding="utf-8") as f: return [line.strip() for line in f if line.strip()] diff --git a/tests/deprecated/test_wiktionary_parser.py b/tests/deprecated/test_wiktionary_parser.py index f52033d6..be62a6d7 100644 --- a/tests/deprecated/test_wiktionary_parser.py +++ b/tests/deprecated/test_wiktionary_parser.py @@ -21,7 +21,7 @@ sys.path.insert(0, str(PROJECT_ROOT / "tests")) -from wiktionary import parse_wikt_definition, WIKT_LANG_MAP +from wiktionary import WIKT_LANG_MAP, parse_wikt_definition from wiktionary_test_utils import is_quality_definition FIXTURES_DIR = PROJECT_ROOT / "tests" / "fixtures" / "wiktionary" @@ -37,7 +37,7 @@ def load_all_fixtures(): fixtures = {} for f in sorted(FIXTURES_DIR.glob("*.json")): lang = f.stem - with open(f, "r", encoding="utf-8") as fh: + with open(f, encoding="utf-8") as fh: fixtures[lang] = json.load(fh) return fixtures @@ -47,7 +47,7 @@ def generate_golden_test_cases(): cases = [] for f in sorted(FIXTURES_DIR.glob("*.json")): lang = f.stem - with open(f, "r", encoding="utf-8") as fh: + with open(f, encoding="utf-8") as fh: data = json.load(fh) for word, info in data.items(): extract = info.get("extract") @@ -98,7 +98,7 @@ def generate_quality_test_cases(): cases = [] for f in sorted(FIXTURES_DIR.glob("*.json")): lang = f.stem - with open(f, "r", encoding="utf-8") as fh: + with open(f, encoding="utf-8") as fh: data = json.load(fh) for word, info in data.items(): parsed = info.get("parsed") diff --git a/tests/test_daily_word.py b/tests/test_daily_word.py index 8fddc41b..7e7664ab 100644 --- a/tests/test_daily_word.py +++ b/tests/test_daily_word.py @@ -9,11 +9,13 @@ - Days > 1681: Consistent hashing algorithm (blocklist-friendly) """ -import pytest import datetime -import random import hashlib +import random from pathlib import Path + +import pytest + from tests.conftest import ALL_LANGUAGES, load_word_list # Migration cutoff - must match webapp/app.py @@ -164,9 +166,9 @@ def test_no_repeat_in_word_list_length_days(self, lang): unique_words = set(selected_words) # All selected words should be unique within the period - assert len(unique_words) == len( - selected_words - ), f"{lang}: Found duplicate words within {num_days} days" + assert len(unique_words) == len(selected_words), ( + f"{lang}: Found duplicate words within {num_days} days" + ) @pytest.mark.parametrize("lang", ALL_LANGUAGES) def test_cycles_through_all_words(self, lang): diff --git a/tests/test_definitions.py b/tests/test_definitions.py index cabf2606..1ec32ca0 100644 --- a/tests/test_definitions.py +++ b/tests/test_definitions.py @@ -5,15 +5,11 @@ """ import json -import os import sys -import tempfile import time from pathlib import Path from unittest.mock import MagicMock, patch -import pytest - # Allow imports from webapp/ directory sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "webapp")) @@ -25,8 +21,6 @@ _call_llm_definition, _wiktionary_url, fetch_definition, - lookup_kaikki_english, - lookup_kaikki_native, ) # --------------------------------------------------------------------------- @@ -330,9 +324,11 @@ def test_kaikki_fallback_when_llm_returns_none(self): "source": "kaikki", "url": None, } - with patch("definitions._call_llm_definition", return_value=None): - with patch("definitions.lookup_kaikki_native", return_value=kaikki_result): - result = fetch_definition("word", "nl", cache_dir=None) + with ( + patch("definitions._call_llm_definition", return_value=None), + patch("definitions.lookup_kaikki_native", return_value=kaikki_result), + ): + result = fetch_definition("word", "nl", cache_dir=None) assert result is not None assert result["source"] == "kaikki" @@ -346,10 +342,12 @@ def test_kaikki_english_fallback_when_native_missing(self): "source": "kaikki-en", "url": None, } - with patch("definitions._call_llm_definition", return_value=None): - with patch("definitions.lookup_kaikki_native", return_value=None): - with patch("definitions.lookup_kaikki_english", return_value=kaikki_en_result): - result = fetch_definition("word", "ro", cache_dir=None) + with ( + patch("definitions._call_llm_definition", return_value=None), + patch("definitions.lookup_kaikki_native", return_value=None), + patch("definitions.lookup_kaikki_english", return_value=kaikki_en_result), + ): + result = fetch_definition("word", "ro", cache_dir=None) assert result is not None assert result["source"] == "kaikki-en" @@ -363,9 +361,11 @@ def test_kaikki_fallback_is_cached(self, tmp_path): "source": "kaikki", "url": None, } - with patch("definitions._call_llm_definition", return_value=None): - with patch("definitions.lookup_kaikki_native", return_value=kaikki_result): - fetch_definition("word", "nl", cache_dir=cache_dir) + with ( + patch("definitions._call_llm_definition", return_value=None), + patch("definitions.lookup_kaikki_native", return_value=kaikki_result), + ): + fetch_definition("word", "nl", cache_dir=cache_dir) cache_file = tmp_path / "nl" / "word.json" assert cache_file.exists() @@ -375,10 +375,12 @@ def test_kaikki_fallback_is_cached(self, tmp_path): def test_negative_cache_only_when_all_tiers_fail(self, tmp_path): """Negative cache is written only when LLM AND kaikki both fail.""" cache_dir = str(tmp_path) - with patch("definitions._call_llm_definition", return_value=None): - with patch("definitions.lookup_kaikki_native", return_value=None): - with patch("definitions.lookup_kaikki_english", return_value=None): - fetch_definition("xyzzy", "zz", cache_dir=cache_dir) + with ( + patch("definitions._call_llm_definition", return_value=None), + patch("definitions.lookup_kaikki_native", return_value=None), + patch("definitions.lookup_kaikki_english", return_value=None), + ): + fetch_definition("xyzzy", "zz", cache_dir=cache_dir) cache_file = tmp_path / "zz" / "xyzzy.json" assert cache_file.exists() diff --git a/tests/test_language_config.py b/tests/test_language_config.py index 6321112e..9312024d 100644 --- a/tests/test_language_config.py +++ b/tests/test_language_config.py @@ -5,13 +5,13 @@ """ import pytest + from tests.conftest import ( ALL_LANGUAGES, - load_language_config, + get_diacritic_base_chars, load_keyboard, + load_language_config, load_word_list, - load_characters, - get_diacritic_base_chars, ) diff --git a/tests/test_word_lists.py b/tests/test_word_lists.py index fb698780..2f43a514 100644 --- a/tests/test_word_lists.py +++ b/tests/test_word_lists.py @@ -9,17 +9,18 @@ """ import pytest + +from scripts.improve_word_lists import is_roman_numeral from tests.conftest import ( ALL_LANGUAGES, - load_word_list, - load_supplement_words, - load_daily_words, + get_diacritic_base_chars, load_blocklist, load_characters, + load_daily_words, load_keyboard, - get_diacritic_base_chars, + load_supplement_words, + load_word_list, ) -from scripts.improve_word_lists import is_roman_numeral class TestWordListBasics: @@ -37,7 +38,7 @@ def test_all_words_are_5_letters(self, lang): words = load_word_list(lang) invalid = [(w, len(w)) for w in words if len(w) != 5] assert not invalid, ( - f"{lang}: Found {len(invalid)} words with wrong length. " f"Examples: {invalid[:5]}" + f"{lang}: Found {len(invalid)} words with wrong length. Examples: {invalid[:5]}" ) @pytest.mark.parametrize("lang", ALL_LANGUAGES) @@ -63,7 +64,7 @@ def test_no_duplicate_words(self, lang): duplicates.append(w) seen.add(w) assert not duplicates, ( - f"{lang}: Found {len(duplicates)} duplicate words. " f"Examples: {duplicates[:10]}" + f"{lang}: Found {len(duplicates)} duplicate words. Examples: {duplicates[:10]}" ) @pytest.mark.parametrize("lang", ALL_LANGUAGES) @@ -72,7 +73,7 @@ def test_words_are_lowercase(self, lang): words = load_word_list(lang) uppercase = [w for w in words if w != w.lower()] assert not uppercase, ( - f"{lang}: Found {len(uppercase)} words with uppercase. " f"Examples: {uppercase[:5]}" + f"{lang}: Found {len(uppercase)} words with uppercase. Examples: {uppercase[:5]}" ) @@ -114,9 +115,9 @@ def test_all_word_chars_in_character_set(self, lang): word_chars.update(word) missing = word_chars - chars - assert ( - not missing - ), f"{lang}: Characters used in words but missing from character set: {missing}" + assert not missing, ( + f"{lang}: Characters used in words but missing from character set: {missing}" + ) class TestKeyboardCoverage: @@ -228,7 +229,7 @@ def test_daily_words_subset_of_main(self, lang): main = set(load_word_list(lang)) invalid = [w for w in daily if w not in main] assert not invalid, ( - f"{lang}: {len(invalid)} daily words not in main list. " f"Examples: {invalid[:5]}" + f"{lang}: {len(invalid)} daily words not in main list. Examples: {invalid[:5]}" ) @pytest.mark.parametrize("lang", ALL_LANGUAGES) @@ -244,7 +245,7 @@ def test_daily_words_no_duplicates(self, lang): duplicates.append(w) seen.add(w) assert not duplicates, ( - f"{lang}: {len(duplicates)} duplicate daily words. " f"Examples: {duplicates[:10]}" + f"{lang}: {len(duplicates)} duplicate daily words. Examples: {duplicates[:10]}" ) @pytest.mark.parametrize("lang", ALL_LANGUAGES) @@ -255,7 +256,7 @@ def test_daily_words_are_5_letters(self, lang): pytest.skip(f"{lang}: No daily words file") invalid = [(w, len(w)) for w in daily if len(w) != 5] assert not invalid, ( - f"{lang}: {len(invalid)} daily words with wrong length. " f"Examples: {invalid[:5]}" + f"{lang}: {len(invalid)} daily words with wrong length. Examples: {invalid[:5]}" ) @pytest.mark.parametrize("lang", ALL_LANGUAGES) @@ -267,8 +268,7 @@ def test_supplement_disjoint_from_main(self, lang): main = set(load_word_list(lang)) overlap = [w for w in supplement if w in main] assert not overlap, ( - f"{lang}: {len(overlap)} supplement words also in main list. " - f"Examples: {overlap[:5]}" + f"{lang}: {len(overlap)} supplement words also in main list. Examples: {overlap[:5]}" ) @@ -320,7 +320,7 @@ def test_daily_words_not_in_blocklist(self, lang): pytest.skip(f"{lang}: No blocklist") overlap = set(daily) & blocklist assert not overlap, ( - f"{lang}: {len(overlap)} daily words in blocklist. " f"Examples: {sorted(overlap)[:10]}" + f"{lang}: {len(overlap)} daily words in blocklist. Examples: {sorted(overlap)[:10]}" ) @pytest.mark.parametrize("lang", ALL_LANGUAGES) @@ -353,7 +353,7 @@ def test_arabic_no_rare_characters_in_daily_words(self): bad_words = [w for w in daily if any(c in rare_chars for c in w)] assert not bad_words, ( f"Arabic: {len(bad_words)} daily words contain characters below " - f"{threshold*100:.0f}% frequency. Rare chars: {rare_chars}. " + f"{threshold * 100:.0f}% frequency. Rare chars: {rare_chars}. " f"Examples: {bad_words[:5]}" ) diff --git a/uv.lock b/uv.lock index 6eb9b206..20bf7e59 100644 --- a/uv.lock +++ b/uv.lock @@ -24,43 +24,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, ] -[[package]] -name = "black" -version = "26.1.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "mypy-extensions" }, - { name = "packaging" }, - { name = "pathspec" }, - { name = "platformdirs" }, - { name = "pytokens" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/30/83/f05f22ff13756e1a8ce7891db517dbc06200796a16326258268f4658a745/black-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3cee1487a9e4c640dc7467aaa543d6c0097c391dc8ac74eb313f2fbf9d7a7cb5", size = 1831956, upload-time = "2026-01-18T04:59:21.38Z" }, - { url = "https://files.pythonhosted.org/packages/7d/f2/b2c570550e39bedc157715e43927360312d6dd677eed2cc149a802577491/black-26.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d62d14ca31c92adf561ebb2e5f2741bf8dea28aef6deb400d49cca011d186c68", size = 1672499, upload-time = "2026-01-18T04:59:23.257Z" }, - { url = "https://files.pythonhosted.org/packages/7a/d7/990d6a94dc9e169f61374b1c3d4f4dd3037e93c2cc12b6f3b12bc663aa7b/black-26.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb1dafbbaa3b1ee8b4550a84425aac8874e5f390200f5502cf3aee4a2acb2f14", size = 1735431, upload-time = "2026-01-18T04:59:24.729Z" }, - { url = "https://files.pythonhosted.org/packages/36/1c/cbd7bae7dd3cb315dfe6eeca802bb56662cc92b89af272e014d98c1f2286/black-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:101540cb2a77c680f4f80e628ae98bd2bd8812fb9d72ade4f8995c5ff019e82c", size = 1400468, upload-time = "2026-01-18T04:59:27.381Z" }, - { url = "https://files.pythonhosted.org/packages/59/b1/9fe6132bb2d0d1f7094613320b56297a108ae19ecf3041d9678aec381b37/black-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f3977a16e347f1b115662be07daa93137259c711e526402aa444d7a88fdc9d4", size = 1207332, upload-time = "2026-01-18T04:59:28.711Z" }, - { url = "https://files.pythonhosted.org/packages/f5/13/710298938a61f0f54cdb4d1c0baeb672c01ff0358712eddaf29f76d32a0b/black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", size = 1878189, upload-time = "2026-01-18T04:59:30.682Z" }, - { url = "https://files.pythonhosted.org/packages/79/a6/5179beaa57e5dbd2ec9f1c64016214057b4265647c62125aa6aeffb05392/black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", size = 1700178, upload-time = "2026-01-18T04:59:32.387Z" }, - { url = "https://files.pythonhosted.org/packages/8c/04/c96f79d7b93e8f09d9298b333ca0d31cd9b2ee6c46c274fd0f531de9dc61/black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", size = 1777029, upload-time = "2026-01-18T04:59:33.767Z" }, - { url = "https://files.pythonhosted.org/packages/49/f9/71c161c4c7aa18bdda3776b66ac2dc07aed62053c7c0ff8bbda8c2624fe2/black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791", size = 1406466, upload-time = "2026-01-18T04:59:35.177Z" }, - { url = "https://files.pythonhosted.org/packages/4a/8b/a7b0f974e473b159d0ac1b6bcefffeb6bec465898a516ee5cc989503cbc7/black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954", size = 1216393, upload-time = "2026-01-18T04:59:37.18Z" }, - { url = "https://files.pythonhosted.org/packages/79/04/fa2f4784f7237279332aa735cdfd5ae2e7730db0072fb2041dadda9ae551/black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", size = 1877781, upload-time = "2026-01-18T04:59:39.054Z" }, - { url = "https://files.pythonhosted.org/packages/cf/ad/5a131b01acc0e5336740a039628c0ab69d60cf09a2c87a4ec49f5826acda/black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", size = 1699670, upload-time = "2026-01-18T04:59:41.005Z" }, - { url = "https://files.pythonhosted.org/packages/da/7c/b05f22964316a52ab6b4265bcd52c0ad2c30d7ca6bd3d0637e438fc32d6e/black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", size = 1775212, upload-time = "2026-01-18T04:59:42.545Z" }, - { url = "https://files.pythonhosted.org/packages/a6/a3/e8d1526bea0446e040193185353920a9506eab60a7d8beb062029129c7d2/black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", size = 1409953, upload-time = "2026-01-18T04:59:44.357Z" }, - { url = "https://files.pythonhosted.org/packages/c7/5a/d62ebf4d8f5e3a1daa54adaab94c107b57be1b1a2f115a0249b41931e188/black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", size = 1217707, upload-time = "2026-01-18T04:59:45.719Z" }, - { url = "https://files.pythonhosted.org/packages/6a/83/be35a175aacfce4b05584ac415fd317dd6c24e93a0af2dcedce0f686f5d8/black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", size = 1871864, upload-time = "2026-01-18T04:59:47.586Z" }, - { url = "https://files.pythonhosted.org/packages/a5/f5/d33696c099450b1274d925a42b7a030cd3ea1f56d72e5ca8bbed5f52759c/black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", size = 1701009, upload-time = "2026-01-18T04:59:49.443Z" }, - { url = "https://files.pythonhosted.org/packages/1b/87/670dd888c537acb53a863bc15abbd85b22b429237d9de1b77c0ed6b79c42/black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", size = 1767806, upload-time = "2026-01-18T04:59:50.769Z" }, - { url = "https://files.pythonhosted.org/packages/fe/9c/cd3deb79bfec5bcf30f9d2100ffeec63eecce826eb63e3961708b9431ff1/black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", size = 1433217, upload-time = "2026-01-18T04:59:52.218Z" }, - { url = "https://files.pythonhosted.org/packages/4e/29/f3be41a1cf502a283506f40f5d27203249d181f7a1a2abce1c6ce188035a/black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", size = 1245773, upload-time = "2026-01-18T04:59:54.457Z" }, - { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" }, -] - [[package]] name = "blinker" version = "1.9.0" @@ -139,21 +102,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4f/af/72ad54402e599152de6d067324c46fe6a4f531c7c65baf7e96c63db55eaf/flask_cors-6.0.2-py3-none-any.whl", hash = "sha256:e57544d415dfd7da89a9564e1e3a9e515042df76e12130641ca6f3f2f03b699a", size = 13257, upload-time = "2025-12-12T20:31:41.3Z" }, ] -[[package]] -name = "flask-flatpages" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "flask" }, - { name = "jinja2" }, - { name = "markdown" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f3/e7/ac2ef938a7f664f47203dae14625a1d25277064b4948152e7d4dcf8887b0/flask_flatpages-0.9.0.tar.gz", hash = "sha256:2290214395a2ef43f9d844db07fc97f314baff807864e67f665ac5c4c938f392", size = 25183, upload-time = "2025-12-31T22:53:25.403Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/58/25/8d7e38bdb198f6eca6328ea68006ce7fbc929aef6d9d98c602433cc8e643/flask_flatpages-0.9.0-py3-none-any.whl", hash = "sha256:35ca9b72a7312117c893ed39577198daa3e575bf14aa8c2d120267b4894267e4", size = 11346, upload-time = "2025-12-31T22:53:24.209Z" }, -] - [[package]] name = "ftfy" version = "6.3.1" @@ -357,15 +305,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/0d/9b6f11382b2f9080b5a366d20e90b4c08e547b6cd08c2a206729e6bad47a/locate-1.1.1-py3-none-any.whl", hash = "sha256:9e5e2f3516639240f4d975c08e95ae6a24ff4dd63d228f927541cdec30105755", size = 5364, upload-time = "2022-12-15T07:01:29.526Z" }, ] -[[package]] -name = "markdown" -version = "3.10.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2b/f4/69fa6ed85ae003c2378ffa8f6d2e3234662abd02c10d216c0ba96081a238/markdown-3.10.2.tar.gz", hash = "sha256:994d51325d25ad8aa7ce4ebaec003febcce822c3f8c911e3b17c52f7f589f950", size = 368805, upload-time = "2026-02-09T14:57:26.942Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/1f/77fa3081e4f66ca3576c896ae5d31c3002ac6607f9747d2e3aa49227e464/markdown-3.10.2-py3-none-any.whl", hash = "sha256:e91464b71ae3ee7afd3017d9f358ef0baf158fd9a298db92f1d4761133824c36", size = 108180, upload-time = "2026-02-09T14:57:25.787Z" }, -] - [[package]] name = "markupsafe" version = "3.0.3" @@ -493,15 +432,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/f2/08ace4142eb281c12701fc3b93a10795e4d4dc7f753911d836675050f886/msgpack-1.1.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d99ef64f349d5ec3293688e91486c5fdb925ed03807f64d98d205d2713c60b46", size = 70868, upload-time = "2025-10-08T09:15:44.959Z" }, ] -[[package]] -name = "mypy-extensions" -version = "1.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, -] - [[package]] name = "openai" version = "2.21.0" @@ -530,15 +460,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] -[[package]] -name = "pathspec" -version = "1.0.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" }, -] - [[package]] name = "pillow" version = "12.1.1" @@ -626,15 +547,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, ] -[[package]] -name = "platformdirs" -version = "4.9.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, -] - [[package]] name = "pluggy" version = "1.6.0" @@ -781,95 +693,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, ] -[[package]] -name = "pytokens" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" }, - { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" }, - { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" }, - { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" }, - { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" }, - { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" }, - { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" }, - { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" }, - { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" }, - { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" }, - { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" }, - { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" }, - { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" }, - { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" }, - { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" }, - { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" }, - { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" }, - { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" }, - { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" }, - { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" }, - { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" }, - { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" }, - { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" }, - { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" }, - { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" }, - { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" }, -] - -[[package]] -name = "pyyaml" -version = "6.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, - { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, - { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" }, - { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" }, - { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" }, - { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" }, - { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" }, - { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" }, - { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, - { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, - { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, - { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, - { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, - { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, - { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, - { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, - { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, - { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, - { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, - { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, - { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, - { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, - { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, - { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, - { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, - { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, - { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, - { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, - { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, - { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, - { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, - { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, - { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, - { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, - { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, - { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, - { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, - { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, -] - [[package]] name = "regex" version = "2026.2.19" @@ -974,6 +797,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3a/45/affdf2d851b42adf3d13fc5b3b059372e9bd299371fd84cf5723c45871fa/regex-2026.2.19-cp314-cp314t-win_arm64.whl", hash = "sha256:a09ae430e94c049dc6957f6baa35ee3418a3a77f3c12b6e02883bd80a2b679b0", size = 274932, upload-time = "2026-02-19T19:03:45.488Z" }, ] +[[package]] +name = "ruff" +version = "0.15.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/df/f8629c19c5318601d3121e230f74cbee7a3732339c52b21daa2b82ef9c7d/ruff-0.15.6.tar.gz", hash = "sha256:8394c7bb153a4e3811a4ecdacd4a8e6a4fa8097028119160dffecdcdf9b56ae4", size = 4597916, upload-time = "2026-03-12T23:05:47.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/2f/4e03a7e5ce99b517e98d3b4951f411de2b0fa8348d39cf446671adcce9a2/ruff-0.15.6-py3-none-linux_armv6l.whl", hash = "sha256:7c98c3b16407b2cf3d0f2b80c80187384bc92c6774d85fefa913ecd941256fff", size = 10508953, upload-time = "2026-03-12T23:05:17.246Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/55bcdc3e9f80bcf39edf0cd272da6fa511a3d94d5a0dd9e0adf76ceebdb4/ruff-0.15.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ee7dcfaad8b282a284df4aa6ddc2741b3f4a18b0555d626805555a820ea181c3", size = 10942257, upload-time = "2026-03-12T23:05:23.076Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/005c29bd1726c0f492bfa215e95154cf480574140cb5f867c797c18c790b/ruff-0.15.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3bd9967851a25f038fc8b9ae88a7fbd1b609f30349231dffaa37b6804923c4bb", size = 10322683, upload-time = "2026-03-12T23:05:33.738Z" }, + { url = "https://files.pythonhosted.org/packages/5f/74/2f861f5fd7cbb2146bddb5501450300ce41562da36d21868c69b7a828169/ruff-0.15.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13f4594b04e42cd24a41da653886b04d2ff87adbf57497ed4f728b0e8a4866f8", size = 10660986, upload-time = "2026-03-12T23:05:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/c1/a1/309f2364a424eccb763cdafc49df843c282609f47fe53aa83f38272389e0/ruff-0.15.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2ed8aea2f3fe57886d3f00ea5b8aae5bf68d5e195f487f037a955ff9fbaac9e", size = 10332177, upload-time = "2026-03-12T23:05:56.145Z" }, + { url = "https://files.pythonhosted.org/packages/30/41/7ebf1d32658b4bab20f8ac80972fb19cd4e2c6b78552be263a680edc55ac/ruff-0.15.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:70789d3e7830b848b548aae96766431c0dc01a6c78c13381f423bf7076c66d15", size = 11170783, upload-time = "2026-03-12T23:06:01.742Z" }, + { url = "https://files.pythonhosted.org/packages/76/be/6d488f6adca047df82cd62c304638bcb00821c36bd4881cfca221561fdfc/ruff-0.15.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:542aaf1de3154cea088ced5a819ce872611256ffe2498e750bbae5247a8114e9", size = 12044201, upload-time = "2026-03-12T23:05:28.697Z" }, + { url = "https://files.pythonhosted.org/packages/71/68/e6f125df4af7e6d0b498f8d373274794bc5156b324e8ab4bf5c1b4fc0ec7/ruff-0.15.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c22e6f02c16cfac3888aa636e9eba857254d15bbacc9906c9689fdecb1953ab", size = 11421561, upload-time = "2026-03-12T23:05:31.236Z" }, + { url = "https://files.pythonhosted.org/packages/f1/9f/f85ef5fd01a52e0b472b26dc1b4bd228b8f6f0435975442ffa4741278703/ruff-0.15.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98893c4c0aadc8e448cfa315bd0cc343a5323d740fe5f28ef8a3f9e21b381f7e", size = 11310928, upload-time = "2026-03-12T23:05:45.288Z" }, + { url = "https://files.pythonhosted.org/packages/8c/26/b75f8c421f5654304b89471ed384ae8c7f42b4dff58fa6ce1626d7f2b59a/ruff-0.15.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:70d263770d234912374493e8cc1e7385c5d49376e41dfa51c5c3453169dc581c", size = 11235186, upload-time = "2026-03-12T23:05:50.677Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d4/d5a6d065962ff7a68a86c9b4f5500f7d101a0792078de636526c0edd40da/ruff-0.15.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:55a1ad63c5a6e54b1f21b7514dfadc0c7fb40093fa22e95143cf3f64ebdcd512", size = 10635231, upload-time = "2026-03-12T23:05:37.044Z" }, + { url = "https://files.pythonhosted.org/packages/d6/56/7c3acf3d50910375349016cf33de24be021532042afbed87942858992491/ruff-0.15.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8dc473ba093c5ec238bb1e7429ee676dca24643c471e11fbaa8a857925b061c0", size = 10340357, upload-time = "2026-03-12T23:06:04.748Z" }, + { url = "https://files.pythonhosted.org/packages/06/54/6faa39e9c1033ff6a3b6e76b5df536931cd30caf64988e112bbf91ef5ce5/ruff-0.15.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:85b042377c2a5561131767974617006f99f7e13c63c111b998f29fc1e58a4cfb", size = 10860583, upload-time = "2026-03-12T23:05:58.978Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/509a201b843b4dfb0b32acdedf68d951d3377988cae43949ba4c4133a96a/ruff-0.15.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cef49e30bc5a86a6a92098a7fbf6e467a234d90b63305d6f3ec01225a9d092e0", size = 11410976, upload-time = "2026-03-12T23:05:39.955Z" }, + { url = "https://files.pythonhosted.org/packages/6c/25/3fc9114abf979a41673ce877c08016f8e660ad6cf508c3957f537d2e9fa9/ruff-0.15.6-py3-none-win32.whl", hash = "sha256:bbf67d39832404812a2d23020dda68fee7f18ce15654e96fb1d3ad21a5fe436c", size = 10616872, upload-time = "2026-03-12T23:05:42.451Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/09ece68445ceac348df06e08bf75db72d0e8427765b96c9c0ffabc1be1d9/ruff-0.15.6-py3-none-win_amd64.whl", hash = "sha256:aee25bc84c2f1007ecb5037dff75cef00414fdf17c23f07dc13e577883dca406", size = 11787271, upload-time = "2026-03-12T23:05:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" }, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -1060,7 +908,6 @@ source = { virtual = "." } dependencies = [ { name = "flask" }, { name = "flask-cors" }, - { name = "flask-flatpages" }, { name = "gunicorn" }, { name = "openai" }, { name = "pillow" }, @@ -1068,8 +915,8 @@ dependencies = [ [package.dev-dependencies] dev = [ - { name = "black" }, { name = "pytest" }, + { name = "ruff" }, { name = "wordfreq" }, ] @@ -1077,7 +924,6 @@ dev = [ requires-dist = [ { name = "flask", specifier = ">=3.1.0" }, { name = "flask-cors", specifier = ">=6.0.0" }, - { name = "flask-flatpages", specifier = ">=0.9.0" }, { name = "gunicorn", specifier = ">=24.0.0" }, { name = "openai", specifier = ">=2.21.0" }, { name = "pillow", specifier = ">=12.1.1" }, @@ -1085,7 +931,7 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ - { name = "black", specifier = ">=24.0.0" }, { name = "pytest", specifier = ">=8.0.0" }, + { name = "ruff", specifier = ">=0.11.0" }, { name = "wordfreq", specifier = ">=3.1.1" }, ] diff --git a/webapp/app.py b/webapp/app.py index 8c5678a1..1d9dd0bd 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -1,28 +1,27 @@ -from flask import ( - Flask, - render_template, - make_response, - redirect, - request, - send_from_directory, - jsonify, - abort, -) -import json -import os import datetime import glob -import random -from zoneinfo import ZoneInfo import hashlib -import re -import urllib.parse -import urllib.request as urlreq +import json import logging +import os +import random +import urllib.request as urlreq from pathlib import Path +from zoneinfo import ZoneInfo + from definitions import ( fetch_definition as _fetch_definition_impl, ) +from flask import ( + Flask, + abort, + jsonify, + make_response, + redirect, + render_template, + request, + send_from_directory, +) # Load .env file if it exists (for local development) _env_path = Path(__file__).resolve().parent.parent / ".env" @@ -70,7 +69,7 @@ def load_vite_manifest(): f"Vite manifest not found at {manifest_path}. " "Run 'pnpm build' first to build frontend assets." ) - with open(manifest_path, "r") as f: + with open(manifest_path) as f: return json.load(f) @@ -121,14 +120,14 @@ def inject_hreflang(): # load other_wordles.json file -with open(f"{data_dir}other_wordles.json", "r") as f: +with open(f"{data_dir}other_wordles.json") as f: other_wordles = json.load(f) def load_characters(lang): if not glob.glob(f"{data_dir}languages/{lang}/{lang}_characters.txt"): characters = set() - with open(f"{data_dir}languages/{lang}/{lang}_5words.txt", "r") as f: + with open(f"{data_dir}languages/{lang}/{lang}_5words.txt") as f: for line in f: characters.update(line.strip()) with open(f"{data_dir}languages/{lang}/{lang}_characters.txt", "w") as f: @@ -138,7 +137,7 @@ def load_characters(lang): for char in characters: f.write(char + "\n") - with open(f"{data_dir}languages/{lang}/{lang}_characters.txt", "r") as f: + with open(f"{data_dir}languages/{lang}/{lang}_characters.txt") as f: characters = [line.strip() for line in f] return characters @@ -150,7 +149,7 @@ def load_characters(lang): def load_words(lang): """loads the words and does some basic QA""" _5words = [] - with open(f"{data_dir}/languages/{lang}/{lang}_5words.txt", "r") as f: + with open(f"{data_dir}/languages/{lang}/{lang}_5words.txt") as f: for line in f: _5words.append(line.strip()) # QA @@ -179,7 +178,7 @@ def load_words(lang): def load_supplemental_words(lang): """loads the supplemental words file if it exists""" try: - with open(f"{data_dir}languages/{lang}/{lang}_5words_supplement.txt", "r") as f: + with open(f"{data_dir}languages/{lang}/{lang}_5words_supplement.txt") as f: supplemental_words = [line.strip() for line in f] supplemental_words = [ word @@ -201,7 +200,7 @@ def load_blocklist(lang): """ blocklist_path = f"{data_dir}languages/{lang}/{lang}_blocklist.txt" try: - with open(blocklist_path, "r") as f: + with open(blocklist_path) as f: blocklist = set() for line in f: line = line.strip() @@ -226,7 +225,7 @@ def load_daily_words(lang): """ daily_path = f"{data_dir}languages/{lang}/{lang}_daily_words.txt" try: - with open(daily_path, "r") as f: + with open(daily_path) as f: daily_words = [] for line in f: line = line.strip() @@ -250,7 +249,7 @@ def load_curated_schedule(lang): """ schedule_path = f"{data_dir}languages/{lang}/{lang}_curated_schedule.txt" try: - with open(schedule_path, "r") as f: + with open(schedule_path) as f: schedule = [] for line in f: line = line.strip() @@ -264,11 +263,11 @@ def load_curated_schedule(lang): def load_language_config(lang): """Load language config, merging with default to ensure all keys exist.""" # Load default config first - with open(f"{data_dir}default_language_config.json", "r") as f: + with open(f"{data_dir}default_language_config.json") as f: default_config = json.load(f) try: - with open(f"{data_dir}languages/{lang}/language_config.json", "r") as f: + with open(f"{data_dir}languages/{lang}/language_config.json") as f: language_config = json.load(f) # Merge: language-specific values override defaults @@ -294,7 +293,7 @@ def load_keyboard(lang): """ keyboard_path = f"{data_dir}languages/{lang}/{lang}_keyboard.json" try: - with open(keyboard_path, "r") as f: + with open(keyboard_path) as f: keyboard_data = json.load(f) except FileNotFoundError: return {"default": None, "layouts": {}} @@ -451,7 +450,7 @@ def idx_to_date(day_idx): language_configs = {l_code: load_language_config(l_code) for l_code in language_codes} # Load default language config for UI translations on homepage -with open(f"{data_dir}default_language_config.json", "r") as f: +with open(f"{data_dir}default_language_config.json") as f: default_language_config = json.load(f) keyboards = {k: load_keyboard(k) for k in language_codes} @@ -485,7 +484,7 @@ def get_word_for_day(lang_code, day_idx): cache_path = os.path.join(WORD_HISTORY_DIR, lang_code, f"{day_idx}.txt") if os.path.exists(cache_path): try: - with open(cache_path, "r") as f: + with open(cache_path) as f: cached = f.read().strip() if cached: return cached @@ -606,25 +605,41 @@ def load_languages(): # print stats about how many languages we have print("\n***********************************************") -print(f" STATS") +print(" STATS") print(f"- {len(languages)} languages") print( - f"- {len([k for (k, v) in language_codes_5words_supplements.items() if v !=[]])} languages with supplemental words" + f"- {len([k for (k, v) in language_codes_5words_supplements.items() if v != []])} languages with supplemental words" ) print( - f"- The language with least words is {min(language_codes_5words, key=lambda k: len(language_codes_5words[k]))}, with {len(language_codes_5words[min(language_codes_5words, key=lambda k: len(language_codes_5words[k]))])} words" + f"- The language with least words is { + min(language_codes_5words, key=lambda k: len(language_codes_5words[k])) + }, with { + len( + language_codes_5words[ + min(language_codes_5words, key=lambda k: len(language_codes_5words[k])) + ] + ) + } words" ) print( - f"- The language with most words is {max(language_codes_5words, key=lambda k: len(language_codes_5words[k]))}, with {len(language_codes_5words[max(language_codes_5words, key=lambda k: len(language_codes_5words[k]))])} words" + f"- The language with most words is { + max(language_codes_5words, key=lambda k: len(language_codes_5words[k])) + }, with { + len( + language_codes_5words[ + max(language_codes_5words, key=lambda k: len(language_codes_5words[k])) + ] + ) + } words" ) print( - f"- Average number of words per language is {sum(len(language_codes_5words[l_code]) for l_code in language_codes)/len(language_codes):.2f}" + f"- Average number of words per language is {sum(len(language_codes_5words[l_code]) for l_code in language_codes) / len(language_codes):.2f}" ) print( - f"- Average length of supplemental words per language is {sum(len(language_codes_5words_supplements[l_code]) for l_code in language_codes)/len(language_codes):.2f}" + f"- Average length of supplemental words per language is {sum(len(language_codes_5words_supplements[l_code]) for l_code in language_codes) / len(language_codes):.2f}" ) print(f"- There are {len(other_wordles)} other wordles") -print(f"***********************************************\n") +print("***********************************************\n") ############################################################################### @@ -818,7 +833,7 @@ def _load_word_stats(lang_code, day_idx): stats_path = os.path.join(WORD_STATS_DIR, lang_code, f"{day_idx}.json") if os.path.exists(stats_path): try: - with open(stats_path, "r") as f: + with open(stats_path) as f: return json.load(f) except Exception: pass @@ -844,7 +859,7 @@ def _update_word_stats(lang_code, day_idx, won, attempts): stats = None if os.path.exists(stats_path): try: - with open(stats_path, "r") as f: + with open(stats_path) as f: stats = json.load(f) except Exception: pass @@ -880,8 +895,8 @@ def _update_word_stats(lang_code, day_idx, won, attempts): def before_request(): if ( request.url.startswith("http://") - and not "localhost" in request.url - and not "127.0.0" in request.url + and "localhost" not in request.url + and "127.0.0" not in request.url ): url = request.url.replace("http://", "https://", 1) code = 301 @@ -958,7 +973,7 @@ def _build_stats_data(): except ValueError: continue # skip non-numeric filenames try: - with open(os.path.join(lang_dir, fname), "r") as f: + with open(os.path.join(lang_dir, fname)) as f: s = json.load(f) lang_total_plays += s.get("total", 0) lang_total_wins += s.get("wins", 0) @@ -1275,7 +1290,7 @@ def generate_word_image(word, definition_hint, api_key, cache_dir, cache_path): os.unlink(tmp_path) return "ok" - except (openai.OpenAIError, urlreq.URLError, IOError, OSError) as e: + except (openai.OpenAIError, urlreq.URLError, OSError) as e: logging.error(f"Image generation failed for {word}: {e}") return "error" diff --git a/webapp/definitions.py b/webapp/definitions.py index a06e9bf7..da1873b2 100644 --- a/webapp/definitions.py +++ b/webapp/definitions.py @@ -235,7 +235,7 @@ def _call_llm_definition(word, lang_code): return None def_en = definition_en[:300] - def_native = ((definition_native or definition_en))[:300] + def_native = (definition_native or definition_en)[:300] wikt_url = _wiktionary_url(word, lang_code) return { "definition_native": def_native, @@ -283,7 +283,7 @@ def fetch_definition(word, lang_code, cache_dir=None, skip_negative_cache=False) if os.path.exists(cache_path): try: - with open(cache_path, "r") as f: + with open(cache_path) as f: loaded = json.load(f) if loaded.get("not_found"): if skip_negative_cache: @@ -314,7 +314,7 @@ def fetch_definition(word, lang_code, cache_dir=None, skip_negative_cache=False) os.makedirs(lang_cache_dir, exist_ok=True) with open(cache_path, "w") as f: json.dump(result or {"not_found": True, "ts": int(time.time())}, f) - except IOError: + except OSError: pass return result