Просмотр исходного кода

Initial import: three approaches to porting mnemonicode to Python

ctypes: Python package wrapping the original C library via ctypes
go: Go port with a Python wrapper
rebuild: pure-Python reconstruction from the C source
ZiB 4 месяцев назад
Сommit
1b1156ff36

+ 3 - 0
.gitignore

@@ -0,0 +1,3 @@
+__pycache__/
+*.pyc
+*.pyo

+ 3 - 0
ctypes/.gitignore

@@ -0,0 +1,3 @@
+__pycache__/
+*.pyc
+*.so

+ 14 - 0
ctypes/Makefile

@@ -0,0 +1,14 @@
+.PHONY: lib dev clean
+
+C_SRCS = mnemonicode/mnemonic.c mnemonicode/mn_wordlist.c
+
+lib: libmnemonic.so
+
+libmnemonic.so: $(C_SRCS) mnemonicode/mnemonic.h
+	$(CC) -Wall -fPIC -shared -o $@ $(C_SRCS)
+
+dev: lib
+	uv run uvicorn mnemonic_api.app:app --reload
+
+clean:
+	rm -f libmnemonic.so

+ 0 - 0
ctypes/README.md


+ 26 - 0
ctypes/mnemonicode/.github/workflows/releaser.yml

@@ -0,0 +1,26 @@
+name: releaser
+
+on:
+    push:
+        tags:
+            - '*.*.*'
+
+jobs:
+    ubuntubuild:
+        runs-on: ubuntu-latest
+        steps:
+            -   name: Checkout
+                uses: actions/checkout@v2
+                with:
+                    fetch-depth: 0
+            -   name: Build
+                run: |
+                  sudo apt-get install -y build-essential gcc mingw-w64-tools zip gzip tar g++-mingw-w64-x86-64
+                  make
+                  tar czf linux-amd64.tar.gz mnencode mndecode
+            - name: Upload release binaries
+              uses: alexellis/upload-assets@0.4.0
+              env:
+                GITHUB_TOKEN: ${{ github.token }}
+              with:
+                asset_paths: '["linux-amd64.tar.gz"]'

+ 3 - 0
ctypes/mnemonicode/.gitignore

@@ -0,0 +1,3 @@
+*.o
+mnencode
+mndecode

+ 19 - 0
ctypes/mnemonicode/COPYING

@@ -0,0 +1,19 @@
+Copyright (c) 2000 Oren Tirosh <oren@hishome.net>
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.

+ 16 - 0
ctypes/mnemonicode/Makefile

@@ -0,0 +1,16 @@
+.PHONY: sample_programs clean
+
+CFLAGS=-Wall -Wextra -ansi -pedantic
+
+sample_programs: mnencode mndecode
+
+mnencode: mnencode.o mnemonic.o mn_wordlist.o
+
+mndecode: mndecode.o mnemonic.o mn_wordlist.o
+
+mn_wordlist.o: mn_wordlist.c mnemonic.h
+
+mnemonic.o: mnemonic.c mnemonic.h
+
+clean: 
+	$(RM) *.o mnencode mndecode *~

+ 110 - 0
ctypes/mnemonicode/README

@@ -0,0 +1,110 @@
+Oren Tirosh is no longer maintaining the original version of this project.
+Stephen Paul Weber likes it and is making it accessible on GitHub.
+
+These routines implement a method for encoding binary data into a sequence
+of words which can be spoken over the phone, for example, and converted
+back to data on the other side.
+
+For more information see <http://web.archive.org/web/20101031205747/http://www.tothink.com/mnemonic/>
+
+There are some other somewhat similar systems that seem less satisfactory:
+
+- OTP was designed for easy typing, and for minimizing length, but as
+  a consequence the word list contains words that are similar ("AD"
+  and "ADD") that are poor for dictating over the phone
+
+- PGPfone has optimized "maximum phonetic distance" between words,
+  which resolves the above problem but has some other drawbacks:
+
+  - Low efficiency, as it encodes a little less than 1 bit per
+    character;
+
+  - Word quality issues, as some words are somewhat obscure to
+    non-native speakers of English, or are awkward to use or type.
+
+Mnemonic tries to do better by being more selective about its word
+list.  Its criteria are thus:
+
+Mandatory Criteria:
+
+ - The wordlist contains 1626 words.
+
+ - All words are between 4 and 7 letters long.
+
+ - No word in the list is a prefix of another word (e.g. visit,
+   visitor).
+
+ - Five letter prefixes of words are sufficient to be unique. 
+
+Less Strict Criteria:
+
+  - The words should be usable by people all over the world. The list
+    is far from perfect in that respect. It is heavily biased towards
+    western culture and English in particular. The international
+    vocabulary is simply not big enough. One can argue that even words
+    like "hotel" or "radio" are not truly international. You will find
+    many English words in the list but I have tried to limit them to
+    words that are part of a beginner's vocabulary or words that have
+    close relatives in other european languages. In some cases a word
+    has a different meaning in another language or is pronounced very
+    differently but for the purpose of the encoding it is still ok - I
+    assume that when the encoding is used for spoken communication
+    both sides speak the same language.
+
+  - The words should have more than one syllable. This makes them
+    easier to recognize when spoken, especially over a phone
+    line. Again, you will find many exceptions. For one syllable words
+    I have tried to use words with 3 or more consonants or words with
+    diphthongs, making for a longer and more distinct
+    pronounciation. As a result of this requirement the average word
+    length has increased. I do not consider this to be a problem since
+    my goal in limiting the word length was not to reduce the average
+    length of encoded data but to limit the maximum length to fit in
+    fixed-size fields or a terminal line width.
+
+  - No two words on the list should sound too much alike. Soundalikes
+    such as "sweet" and "suite" are ruled out. One of the two is
+    chosen and the other should be accepted by the decoder's
+    soundalike matching code or using explicit aliases for some words.
+
+  - No offensive words. The rule was to avoid words that I would not
+    like to be printed on my business card. I have extended this to
+    words that by themselves are not offensive but are too likely to
+    create combinations that someone may find embarrassing or
+    offensive. This includes words dealing with religion such as
+    "church" or "jewish" and some words with negative meanings like
+    "problem" or "fiasco". I am sure that a creative mind (or a random
+    number generator) can find plenty of embarrasing or offensive word
+    combinations using only words in the list but I have tried to
+    avoid the more obvious ones. One of my tools for this was simply a
+    generator of random word combinations - the problematic ones stick
+    out like a sore thumb.
+
+  - Avoid words with tricky spelling or pronounciation. Even if the
+    receiver of the message can probably spell the word close enough
+    for the soundalike matcher to recognize it correctly I prefer
+    avoiding such words. I believe this will help users feel more
+    comfortable using the system, increase the level of confidence and
+    decrease the overall error rate. Most words in the list can be
+    spelled more or less correctly from hearing, even without knowing
+    the word.
+
+  - The word should feel right for the job. I know, this one is very
+    subjective but some words would meet all the criteria and still
+    not feel right for the purpose of mnemonic encoding. The word
+    should feel like one of the words in the radio phonetic alphabets
+    (alpha, bravo, charlie, delta etc).
+
+mnemonic.h	Header file
+mnemonic.c	Encoding/decoding and associated routines
+mn_wordlist.c	The word list itself
+mnencode.c	Sample program - encode data from stdin to stdout
+mndecode.c	Sample program - decode data from stdin to stdout
+
+== Other Implementations ==
+
+Elixr:      <https://github.com/mwmiller/mnemonex>
+Go:         <https://bitbucket.org/dchapes/mnemonicode>
+JavaScript: <https://github.com/mbrubeck/mnemonic.js>
+Python:     <https://github.com/bwhmather/python-mnemonicode>
+Rust:       <https://github.com/mbrubeck/rust-mnemonic>

+ 302 - 0
ctypes/mnemonicode/mn_wordlist.c

@@ -0,0 +1,302 @@
+/* mn_wordlist.c
+ Copyright (c) 2000  Oren Tirosh <oren@hishome.net>
+ 
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+ 
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+ 
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+*/
+
+#include "mnemonic.h"
+
+const char *mn_wordlist_version =
+  " Wordlist ver 0.7";
+
+const char *mn_words[MN_WORDS + 1] = { 0,
+  "academy",  "acrobat",  "active",   "actor",    "adam",     "admiral",  
+  "adrian",   "africa",   "agenda",   "agent",    "airline",  "airport",  
+  "aladdin",  "alarm",    "alaska",   "albert",   "albino",   "album",    
+  "alcohol",  "alex",     "algebra",  "alibi",    "alice",    "alien",    
+  "alpha",    "alpine",   "amadeus",  "amanda",   "amazon",   "amber",    
+  "america",  "amigo",    "analog",   "anatomy",  "angel",    "animal",   
+  "antenna",  "antonio",  "apollo",   "april",    "archive",  "arctic",   
+  "arizona",  "arnold",   "aroma",    "arthur",   "artist",   "asia",     
+  "aspect",   "aspirin",  "athena",   "athlete",  "atlas",    "audio",    
+  "august",   "austria",  "axiom",    "aztec",    "balance",  "ballad",   
+  "banana",   "bandit",   "banjo",    "barcode",  "baron",    "basic",    
+  "battery",  "belgium",  "berlin",   "bermuda",  "bernard",  "bikini",   
+  "binary",   "bingo",    "biology",  "block",    "blonde",   "bonus",    
+  "boris",    "boston",   "boxer",    "brandy",   "bravo",    "brazil",   
+  "bronze",   "brown",    "bruce",    "bruno",    "burger",   "burma",    
+  "cabinet",  "cactus",   "cafe",     "cairo",    "cake",     "calypso",  
+  "camel",    "camera",   "campus",   "canada",   "canal",    "cannon",   
+  "canoe",    "cantina",  "canvas",   "canyon",   "capital",  "caramel",  
+  "caravan",  "carbon",   "cargo",    "carlo",    "carol",    "carpet",   
+  "cartel",   "casino",   "castle",   "castro",   "catalog",  "caviar",   
+  "cecilia",  "cement",   "center",   "century",  "ceramic",  "chamber",  
+  "chance",   "change",   "chaos",    "charlie",  "charm",    "charter",  
+  "chef",     "chemist",  "cherry",   "chess",    "chicago",  "chicken",  
+  "chief",    "china",    "cigar",    "cinema",   "circus",   "citizen",  
+  "city",     "clara",    "classic",  "claudia",  "clean",    "client",   
+  "climax",   "clinic",   "clock",    "club",     "cobra",    "coconut",  
+  "cola",     "collect",  "colombo",  "colony",   "color",    "combat",   
+  "comedy",   "comet",    "command",  "compact",  "company",  "complex",  
+  "concept",  "concert",  "connect",  "consul",   "contact",  "context",  
+  "contour",  "control",  "convert",  "copy",     "corner",   "corona",   
+  "correct",  "cosmos",   "couple",   "courage",  "cowboy",   "craft",    
+  "crash",    "credit",   "cricket",  "critic",   "crown",    "crystal",  
+  "cuba",     "culture",  "dallas",   "dance",    "daniel",   "david",    
+  "decade",   "decimal",  "deliver",  "delta",    "deluxe",   "demand",   
+  "demo",     "denmark",  "derby",    "design",   "detect",   "develop",  
+  "diagram",  "dialog",   "diamond",  "diana",    "diego",    "diesel",   
+  "diet",     "digital",  "dilemma",  "diploma",  "direct",   "disco",    
+  "disney",   "distant",  "doctor",   "dollar",   "dominic",  "domino",   
+  "donald",   "dragon",   "drama",    "dublin",   "duet",     "dynamic",  
+  "east",     "ecology",  "economy",  "edgar",    "egypt",    "elastic",  
+  "elegant",  "element",  "elite",    "elvis",    "email",    "energy",   
+  "engine",   "english",  "episode",  "equator",  "escort",   "ethnic",   
+  "europe",   "everest",  "evident",  "exact",    "example",  "exit",     
+  "exotic",   "export",   "express",  "extra",    "fabric",   "factor",   
+  "falcon",   "family",   "fantasy",  "fashion",  "fiber",    "fiction",  
+  "fidel",    "fiesta",   "figure",   "film",     "filter",   "final",    
+  "finance",  "finish",   "finland",  "flash",    "florida",  "flower",   
+  "fluid",    "flute",    "focus",    "ford",     "forest",   "formal",   
+  "format",   "formula",  "fortune",  "forum",    "fragile",  "france",   
+  "frank",    "friend",   "frozen",   "future",   "gabriel",  "galaxy",   
+  "gallery",  "gamma",    "garage",   "garden",   "garlic",   "gemini",   
+  "general",  "genetic",  "genius",   "germany",  "global",   "gloria",   
+  "golf",     "gondola",  "gong",     "good",     "gordon",   "gorilla",  
+  "grand",    "granite",  "graph",    "green",    "group",    "guide",    
+  "guitar",   "guru",     "hand",     "happy",    "harbor",   "harmony",  
+  "harvard",  "havana",   "hawaii",   "helena",   "hello",    "henry",    
+  "hilton",   "history",  "horizon",  "hotel",    "human",    "humor",    
+  "icon",     "idea",     "igloo",    "igor",     "image",    "impact",   
+  "import",   "index",    "india",    "indigo",   "input",    "insect",   
+  "instant",  "iris",     "italian",  "jacket",   "jacob",    "jaguar",   
+  "janet",    "japan",    "jargon",   "jazz",     "jeep",     "john",     
+  "joker",    "jordan",   "jumbo",    "june",     "jungle",   "junior",   
+  "jupiter",  "karate",   "karma",    "kayak",    "kermit",   "kilo",     
+  "king",     "koala",    "korea",    "labor",    "lady",     "lagoon",   
+  "laptop",   "laser",    "latin",    "lava",     "lecture",  "left",     
+  "legal",    "lemon",    "level",    "lexicon",  "liberal",  "libra",    
+  "limbo",    "limit",    "linda",    "linear",   "lion",     "liquid",   
+  "liter",    "little",   "llama",    "lobby",    "lobster",  "local",    
+  "logic",    "logo",     "lola",     "london",   "lotus",    "lucas",    
+  "lunar",    "machine",  "macro",    "madam",    "madonna",  "madrid",   
+  "maestro",  "magic",    "magnet",   "magnum",   "major",    "mama",     
+  "mambo",    "manager",  "mango",    "manila",   "marco",    "marina",   
+  "market",   "mars",     "martin",   "marvin",   "master",   "matrix",   
+  "maximum",  "media",    "medical",  "mega",     "melody",   "melon",    
+  "memo",     "mental",   "mentor",   "menu",     "mercury",  "message",  
+  "metal",    "meteor",   "meter",    "method",   "metro",    "mexico",   
+  "miami",    "micro",    "million",  "mineral",  "minimum",  "minus",    
+  "minute",   "miracle",  "mirage",   "miranda",  "mister",   "mixer",    
+  "mobile",   "model",    "modem",    "modern",   "modular",  "moment",   
+  "monaco",   "monica",   "monitor",  "mono",     "monster",  "montana",  
+  "morgan",   "motel",    "motif",    "motor",    "mozart",   "multi",    
+  "museum",   "music",    "mustang",  "natural",  "neon",     "nepal",    
+  "neptune",  "nerve",    "neutral",  "nevada",   "news",     "ninja",    
+  "nirvana",  "normal",   "nova",     "novel",    "nuclear",  "numeric",  
+  "nylon",    "oasis",    "object",   "observe",  "ocean",    "octopus",  
+  "olivia",   "olympic",  "omega",    "opera",    "optic",    "optimal",  
+  "orange",   "orbit",    "organic",  "orient",   "origin",   "orlando",  
+  "oscar",    "oxford",   "oxygen",   "ozone",    "pablo",    "pacific",  
+  "pagoda",   "palace",   "pamela",   "panama",   "panda",    "panel",    
+  "panic",    "paradox",  "pardon",   "paris",    "parker",   "parking",  
+  "parody",   "partner",  "passage",  "passive",  "pasta",    "pastel",   
+  "patent",   "patriot",  "patrol",   "patron",   "pegasus",  "pelican",  
+  "penguin",  "pepper",   "percent",  "perfect",  "perfume",  "period",   
+  "permit",   "person",   "peru",     "phone",    "photo",    "piano",    
+  "picasso",  "picnic",   "picture",  "pigment",  "pilgrim",  "pilot",    
+  "pirate",   "pixel",    "pizza",    "planet",   "plasma",   "plaster",  
+  "plastic",  "plaza",    "pocket",   "poem",     "poetic",   "poker",    
+  "polaris",  "police",   "politic",  "polo",     "polygon",  "pony",     
+  "popcorn",  "popular",  "postage",  "postal",   "precise",  "prefix",   
+  "premium",  "present",  "price",    "prince",   "printer",  "prism",    
+  "private",  "product",  "profile",  "program",  "project",  "protect",  
+  "proton",   "public",   "pulse",    "puma",     "pyramid",  "queen",    
+  "radar",    "radio",    "random",   "rapid",    "rebel",    "record",   
+  "recycle",  "reflex",   "reform",   "regard",   "regular",  "relax",    
+  "report",   "reptile",  "reverse",  "ricardo",  "ringo",    "ritual",   
+  "robert",   "robot",    "rocket",   "rodeo",    "romeo",    "royal",    
+  "russian",  "safari",   "salad",    "salami",   "salmon",   "salon",    
+  "salute",   "samba",    "sandra",   "santana",  "sardine",  "school",   
+  "screen",   "script",   "second",   "secret",   "section",  "segment",  
+  "select",   "seminar",  "senator",  "senior",   "sensor",   "serial",   
+  "service",  "sheriff",  "shock",    "sierra",   "signal",   "silicon",  
+  "silver",   "similar",  "simon",    "single",   "siren",    "slogan",   
+  "social",   "soda",     "solar",    "solid",    "solo",     "sonic",    
+  "soviet",   "special",  "speed",    "spiral",   "spirit",   "sport",    
+  "static",   "station",  "status",   "stereo",   "stone",    "stop",     
+  "street",   "strong",   "student",  "studio",   "style",    "subject",  
+  "sultan",   "super",    "susan",    "sushi",    "suzuki",   "switch",   
+  "symbol",   "system",   "tactic",   "tahiti",   "talent",   "tango",    
+  "tarzan",   "taxi",     "telex",    "tempo",    "tennis",   "texas",    
+  "textile",  "theory",   "thermos",  "tiger",    "titanic",  "tokyo",    
+  "tomato",   "topic",    "tornado",  "toronto",  "torpedo",  "total",    
+  "totem",    "tourist",  "tractor",  "traffic",  "transit",  "trapeze",  
+  "travel",   "tribal",   "trick",    "trident",  "trilogy",  "tripod",   
+  "tropic",   "trumpet",  "tulip",    "tuna",     "turbo",    "twist",    
+  "ultra",    "uniform",  "union",    "uranium",  "vacuum",   "valid",    
+  "vampire",  "vanilla",  "vatican",  "velvet",   "ventura",  "venus",    
+  "vertigo",  "veteran",  "victor",   "video",    "vienna",   "viking",   
+  "village",  "vincent",  "violet",   "violin",   "virtual",  "virus",    
+  "visa",     "vision",   "visitor",  "visual",   "vitamin",  "viva",     
+  "vocal",    "vodka",    "volcano",  "voltage",  "volume",   "voyage",   
+  "water",    "weekend",  "welcome",  "western",  "window",   "winter",   
+  "wizard",   "wolf",     "world",    "xray",     "yankee",   "yoga",     
+  "yogurt",   "yoyo",     "zebra",    "zero",     "zigzag",   "zipper",   
+  "zodiac",   "zoom",     "abraham",  "action",   "address",  "alabama",  
+  "alfred",   "almond",   "ammonia",  "analyze",  "annual",   "answer",   
+  "apple",    "arena",    "armada",   "arsenal",  "atlanta",  "atomic",   
+  "avenue",   "average",  "bagel",    "baker",    "ballet",   "bambino",  
+  "bamboo",   "barbara",  "basket",   "bazaar",   "benefit",  "bicycle",  
+  "bishop",   "blitz",    "bonjour",  "bottle",   "bridge",   "british",  
+  "brother",  "brush",    "budget",   "cabaret",  "cadet",    "candle",   
+  "capitan",  "capsule",  "career",   "cartoon",  "channel",  "chapter",  
+  "cheese",   "circle",   "cobalt",   "cockpit",  "college",  "compass",  
+  "comrade",  "condor",   "crimson",  "cyclone",  "darwin",   "declare",  
+  "degree",   "delete",   "delphi",   "denver",   "desert",   "divide",   
+  "dolby",    "domain",   "domingo",  "double",   "drink",    "driver",   
+  "eagle",    "earth",    "echo",     "eclipse",  "editor",   "educate",  
+  "edward",   "effect",   "electra",  "emerald",  "emotion",  "empire",   
+  "empty",    "escape",   "eternal",  "evening",  "exhibit",  "expand",   
+  "explore",  "extreme",  "ferrari",  "first",    "flag",     "folio",    
+  "forget",   "forward",  "freedom",  "fresh",    "friday",   "fuji",     
+  "galileo",  "garcia",   "genesis",  "gold",     "gravity",  "habitat",  
+  "hamlet",   "harlem",   "helium",   "holiday",  "house",    "hunter",   
+  "ibiza",    "iceberg",  "imagine",  "infant",   "isotope",  "jackson",  
+  "jamaica",  "jasmine",  "java",     "jessica",  "judo",     "kitchen",  
+  "lazarus",  "letter",   "license",  "lithium",  "loyal",    "lucky",    
+  "magenta",  "mailbox",  "manual",   "marble",   "mary",     "maxwell",  
+  "mayor",    "milk",     "monarch",  "monday",   "money",    "morning",  
+  "mother",   "mystery",  "native",   "nectar",   "nelson",   "network",  
+  "next",     "nikita",   "nobel",    "nobody",   "nominal",  "norway",   
+  "nothing",  "number",   "october",  "office",   "oliver",   "opinion",  
+  "option",   "order",    "outside",  "package",  "pancake",  "pandora",  
+  "panther",  "papa",     "patient",  "pattern",  "pedro",    "pencil",   
+  "people",   "phantom",  "philips",  "pioneer",  "pluto",    "podium",   
+  "portal",   "potato",   "prize",    "process",  "protein",  "proxy",    
+  "pump",     "pupil",    "python",   "quality",  "quarter",  "quiet",    
+  "rabbit",   "radical",  "radius",   "rainbow",  "ralph",    "ramirez",  
+  "ravioli",  "raymond",  "respect",  "respond",  "result",   "resume",   
+  "retro",    "richard",  "right",    "risk",     "river",    "roger",    
+  "roman",    "rondo",    "sabrina",  "salary",   "salsa",    "sample",   
+  "samuel",   "saturn",   "savage",   "scarlet",  "scoop",    "scorpio",  
+  "scratch",  "scroll",   "sector",   "serpent",  "shadow",   "shampoo",  
+  "sharon",   "sharp",    "short",    "shrink",   "silence",  "silk",     
+  "simple",   "slang",    "smart",    "smoke",    "snake",    "society",  
+  "sonar",    "sonata",   "soprano",  "source",   "sparta",   "sphere",   
+  "spider",   "sponsor",  "spring",   "acid",     "adios",    "agatha",   
+  "alamo",    "alert",    "almanac",  "aloha",    "andrea",   "anita",    
+  "arcade",   "aurora",   "avalon",   "baby",     "baggage",  "balloon",  
+  "bank",     "basil",    "begin",    "biscuit",  "blue",     "bombay",   
+  "brain",    "brenda",   "brigade",  "cable",    "carmen",   "cello",    
+  "celtic",   "chariot",  "chrome",   "citrus",   "civil",    "cloud",    
+  "common",   "compare",  "cool",     "copper",   "coral",    "crater",   
+  "cubic",    "cupid",    "cycle",    "depend",   "door",     "dream",    
+  "dynasty",  "edison",   "edition",  "enigma",   "equal",    "eric",     
+  "event",    "evita",    "exodus",   "extend",   "famous",   "farmer",   
+  "food",     "fossil",   "frog",     "fruit",    "geneva",   "gentle",   
+  "george",   "giant",    "gilbert",  "gossip",   "gram",     "greek",    
+  "grille",   "hammer",   "harvest",  "hazard",   "heaven",   "herbert",  
+  "heroic",   "hexagon",  "husband",  "immune",   "inca",     "inch",     
+  "initial",  "isabel",   "ivory",    "jason",    "jerome",   "joel",     
+  "joshua",   "journal",  "judge",    "juliet",   "jump",     "justice",  
+  "kimono",   "kinetic",  "leonid",   "lima",     "maze",     "medusa",   
+  "member",   "memphis",  "michael",  "miguel",   "milan",    "mile",     
+  "miller",   "mimic",    "mimosa",   "mission",  "monkey",   "moral",    
+  "moses",    "mouse",    "nancy",    "natasha",  "nebula",   "nickel",   
+  "nina",     "noise",    "orchid",   "oregano",  "origami",  "orinoco",  
+  "orion",    "othello",  "paper",    "paprika",  "prelude",  "prepare",  
+  "pretend",  "profit",   "promise",  "provide",  "puzzle",   "remote",   
+  "repair",   "reply",    "rival",    "riviera",  "robin",    "rose",     
+  "rover",    "rudolf",   "saga",     "sahara",   "scholar",  "shelter",  
+  "ship",     "shoe",     "sigma",    "sister",   "sleep",    "smile",    
+  "spain",    "spark",    "split",    "spray",    "square",   "stadium",  
+  "star",     "storm",    "story",    "strange",  "stretch",  "stuart",   
+  "subway",   "sugar",    "sulfur",   "summer",   "survive",  "sweet",    
+  "swim",     "table",    "taboo",    "target",   "teacher",  "telecom",  
+  "temple",   "tibet",    "ticket",   "tina",     "today",    "toga",     
+  "tommy",    "tower",    "trivial",  "tunnel",   "turtle",   "twin",     
+  "uncle",    "unicorn",  "unique",   "update",   "valery",   "vega",     
+  "version",  "voodoo",   "warning",  "william",  "wonder",   "year",     
+  "yellow",   "young",    "absent",   "absorb",   "accent",   "alfonso",  
+  "alias",    "ambient",  "andy",     "anvil",    "appear",   "apropos",  
+  "archer",   "ariel",    "armor",    "arrow",    "austin",   "avatar",   
+  "axis",     "baboon",   "bahama",   "bali",     "balsa",    "bazooka",  
+  "beach",    "beast",    "beatles",  "beauty",   "before",   "benny",    
+  "betty",    "between",  "beyond",   "billy",    "bison",    "blast",    
+  "bless",    "bogart",   "bonanza",  "book",     "border",   "brave",    
+  "bread",    "break",    "broken",   "bucket",   "buenos",   "buffalo",  
+  "bundle",   "button",   "buzzer",   "byte",     "caesar",   "camilla",  
+  "canary",   "candid",   "carrot",   "cave",     "chant",    "child",    
+  "choice",   "chris",    "cipher",   "clarion",  "clark",    "clever",   
+  "cliff",    "clone",    "conan",    "conduct",  "congo",    "content",  
+  "costume",  "cotton",   "cover",    "crack",    "current",  "danube",   
+  "data",     "decide",   "desire",   "detail",   "dexter",   "dinner",   
+  "dispute",  "donor",    "druid",    "drum",     "easy",     "eddie",    
+  "enjoy",    "enrico",   "epoxy",    "erosion",  "except",   "exile",    
+  "explain",  "fame",     "fast",     "father",   "felix",    "field",    
+  "fiona",    "fire",     "fish",     "flame",    "flex",     "flipper",  
+  "float",    "flood",    "floor",    "forbid",   "forever",  "fractal",  
+  "frame",    "freddie",  "front",    "fuel",     "gallop",   "game",     
+  "garbo",    "gate",     "gibson",   "ginger",   "giraffe",  "gizmo",    
+  "glass",    "goblin",   "gopher",   "grace",    "gray",     "gregory",  
+  "grid",     "griffin",  "ground",   "guest",    "gustav",   "gyro",     
+  "hair",     "halt",     "harris",   "heart",    "heavy",    "herman",   
+  "hippie",   "hobby",    "honey",    "hope",     "horse",    "hostel",   
+  "hydro",    "imitate",  "info",     "ingrid",   "inside",   "invent",   
+  "invest",   "invite",   "iron",     "ivan",     "james",    "jester",   
+  "jimmy",    "join",     "joseph",   "juice",    "julius",   "july",     
+  "justin",   "kansas",   "karl",     "kevin",    "kiwi",     "ladder",   
+  "lake",     "laura",    "learn",    "legacy",   "legend",   "lesson",   
+  "life",     "light",    "list",     "locate",   "lopez",    "lorenzo",  
+  "love",     "lunch",    "malta",    "mammal",   "margo",    "marion",   
+  "mask",     "match",    "mayday",   "meaning",  "mercy",    "middle",   
+  "mike",     "mirror",   "modest",   "morph",    "morris",   "nadia",    
+  "nato",     "navy",     "needle",   "neuron",   "never",    "newton",   
+  "nice",     "night",    "nissan",   "nitro",    "nixon",    "north",    
+  "oberon",   "octavia",  "ohio",     "olga",     "open",     "opus",     
+  "orca",     "oval",     "owner",    "page",     "paint",    "palma",    
+  "parade",   "parent",   "parole",   "paul",     "peace",    "pearl",    
+  "perform",  "phoenix",  "phrase",   "pierre",   "pinball",  "place",    
+  "plate",    "plato",    "plume",    "pogo",     "point",    "polite",   
+  "polka",    "poncho",   "powder",   "prague",   "press",    "presto",   
+  "pretty",   "prime",    "promo",    "quasi",    "quest",    "quick",    
+  "quiz",     "quota",    "race",     "rachel",   "raja",     "ranger",   
+  "region",   "remark",   "rent",     "reward",   "rhino",    "ribbon",   
+  "rider",    "road",     "rodent",   "round",    "rubber",   "ruby",     
+  "rufus",    "sabine",   "saddle",   "sailor",   "saint",    "salt",     
+  "satire",   "scale",    "scuba",    "season",   "secure",   "shake",    
+  "shallow",  "shannon",  "shave",    "shelf",    "sherman",  "shine",    
+  "shirt",    "side",     "sinatra",  "sincere",  "size",     "slalom",   
+  "slow",     "small",    "snow",     "sofia",    "song",     "sound",    
+  "south",    "speech",   "spell",    "spend",    "spoon",    "stage",    
+  "stamp",    "stand",    "state",    "stella",   "stick",    "sting",    
+  "stock",    "store",    "sunday",   "sunset",   "support",  "sweden",   
+  "swing",    "tape",     "think",    "thomas",   "tictac",   "time",     
+  "toast",    "tobacco",  "tonight",  "torch",    "torso",    "touch",    
+  "toyota",   "trade",    "tribune",  "trinity",  "triton",   "truck",    
+  "trust",    "type",     "under",    "unit",     "urban",    "urgent",   
+  "user",     "value",    "vendor",   "venice",   "verona",   "vibrate",  
+  "virgo",    "visible",  "vista",    "vital",    "voice",    "vortex",   
+  "waiter",   "watch",    "wave",     "weather",  "wedding",  "wheel",    
+  "whiskey",  "wisdom",   "deal",     "null",     "nurse",    "quebec",   
+  "reserve",  "reunion",  "roof",     "singer",   "verbal",   "amen",     
+  "ego",      "fax",      "jet",      "job",      "rio",      "ski",      
+  "yes"
+};    

+ 35 - 0
ctypes/mnemonicode/mndecode.c

@@ -0,0 +1,35 @@
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include "mnemonic.h"
+
+int main(int argc, char **argv) {
+	char buf[0x90000];
+	mn_byte outbuf[0x10000];
+	int buflen;
+	int n, i;
+	int hex_encoded_output = 0;
+
+	if(argc > 1 && strcmp(argv[1],"-x") == 0) {
+		hex_encoded_output = 1;
+	}
+
+	buflen = fread(buf, 1, sizeof buf - 1, stdin);
+	buf[buflen] = 0;
+
+	n = mn_decode(buf, outbuf, sizeof outbuf);
+	if(n < 0) {
+		fprintf(stderr, "mn_decode result %d\n", n);
+		return EXIT_FAILURE;
+	}
+
+	if(hex_encoded_output) {
+		for(i = 0; i < n; i++) {
+			printf("%02X", outbuf[i]);
+		}
+	} else {
+		fwrite(outbuf, 1, n, stdout);
+	}
+
+	return EXIT_SUCCESS;
+}

+ 462 - 0
ctypes/mnemonicode/mnemonic.c

@@ -0,0 +1,462 @@
+/* mnemonic.c
+
+ Copyright (c) 2000  Oren Tirosh <oren@hishome.net>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+
+*/
+
+#include <ctype.h>
+#include <string.h>
+
+#include "mnemonic.h"
+
+/*
+ * mn_words_required
+ * 
+ * Description:
+ *  Calculate the number of words required to encode data using mnemonic
+ *  encoding.
+ *
+ * Parameters:
+ *  size - Size in bytes of data to be encoded
+ * 
+ * Return value:
+ *  number of words required for the encoding
+ */
+
+int
+mn_words_required (int size)
+{
+  return ((size + 1) * 3) / 4;
+}
+
+
+/*
+ * mn_encode_word_index
+ *
+ * Description:
+ *  Perform one step of encoding binary data into words. Returns word index.
+ *
+ * Parameters:
+ *   src - Pointer to data buffer to encode
+ *   srcsize - Size in bytes of data to encode 
+ *   n - Sequence number of word to encode
+ *       0 <= n < mn_words_required(srcsize)
+ *
+ * Return value:
+ *   0 - no more words to encode / n is out of range
+ *   1..MN_WORDS - word index. May be used as index to the mn_words[] array
+ */
+
+mn_index mn_encode_word_index (void *vsrc, int srcsize, int n)
+{
+  mn_word32 x = 0;		/* Temporary for MN_BASE arithmetic */
+  int offset;			/* Offset into src */
+  int remaining;		/* Octets remaining to end of src */
+  int extra = 0;		/* Index 7 extra words for 24 bit data */
+  int i;
+  mn_byte *src = vsrc;
+
+  if (n < 0 || n >= mn_words_required (srcsize))
+    return 0;			/* word out of range */
+  offset = (n / 3) * 4;		/* byte offset into src */
+  remaining = srcsize - offset;
+  if (remaining <= 0)
+    return 0;
+  if (remaining >= 4)
+    remaining = 4;
+  for (i = 0; i < remaining; i++)
+    x |= (unsigned long)src[offset + i] << (i * 8);	/* endianness-agnostic */
+
+  switch (n % 3)
+    {
+    case 2:			/* Third word of group */
+      if (remaining == 3)	/*  special case for 24 bits */
+	extra = MN_BASE;	/*  use one of the 7 3-letter words */
+      x /= (MN_BASE * MN_BASE);
+      break;
+    case 1:			/* Second word of group */
+      x /= MN_BASE;
+    }
+  return x % MN_BASE + extra + 1;
+}
+
+
+/*
+ * mn_encode_word
+ *
+ * Description:
+ *  Perform one step of encoding binary data into words. Returns pointer 
+ *  to word.
+ *
+ * Parameters:
+ *   src - Pointer to data buffer to encode
+ *   srcsize - Size of data to encode in bytes
+ *   n - Sequence number of word to encode. 
+ *       0 <= n < mn_words_required(srcsize)
+ *
+ * Return value:
+ *   NULL - no more words to encode / n is out of range
+ *   valid pointer - pointer to null-terminated lowercase word. length<=7
+ */
+
+const char *
+mn_encode_word (void *src, int srcsize, int n)
+{
+  return mn_words[mn_encode_word_index (src, srcsize, n)];
+}
+
+
+/*
+ * mn_next_word_index
+ *
+ * Description:
+ *  Perform one step of decoding a null-terminated buffer into word indices.
+ *  A word is defined as a sequence of letter character separated by one
+ *  or more non-letter separator characters.
+ *
+ * Parameters:
+ *  ptr - Pointer to a pointer to the next character in the buffer.
+ *  *ptr is modified by the function; see Return Value below.
+ *
+ * Return value:
+ *  0  - If *ptr==0 (points to the null at the end of the buffer) no more 
+ *       words were found in the buffer. Otherwise *ptr points to beginning 
+ *       of an unrecognized word.
+ *  >0 - index of word found, suitable for decoding with mn_decode_word_index
+ *       or comparison to values returned from mn_encode_index. *ptr points 
+ *       to first character of next word or to terminating null.
+ */
+
+mn_index
+mn_next_word_index (char **ptr)
+{
+  char *wordstart;
+  char wordbuf[MN_WORD_BUFLEN];
+  int i = 0;
+  char c;
+  mn_index idx;
+
+  while (**ptr && !isalpha (**ptr))	/* skip separator chars */
+    (*ptr)++;
+  wordstart = *ptr;		/* save for error reporting */
+  while (**ptr && isalpha (**ptr) && i < MN_WORD_BUFLEN - 1)
+    {
+      c = *(*ptr)++;
+      if (c >= 'A' && c <= 'Z')
+	c += 'a' - 'A';		/* convert to lowercase */
+      wordbuf[i++] = c;
+    }
+  wordbuf[i] = '\0';
+  while (**ptr && isalpha (**ptr))	/* skip tail of long words */
+    (*ptr)++;
+  while (**ptr && !isalpha (**ptr))	/* skip separators */
+    (*ptr)++;
+
+  if (wordbuf[0] == '\0')
+    return 0;			/* EOF, no word found */
+
+  for (idx = 1; idx <= MN_WORDS; idx++)
+    {
+      if (!strcmp (wordbuf, mn_words[idx]))
+	return idx;
+      /* FIXME: some fancy code should go here
+         to accept misspellings and soundalikes.
+         (replacing the linear search would also be nice) */
+    }
+  *ptr = wordstart;
+  return 0;			/* not found */
+}
+
+
+/*
+ * mn_decode_word_index
+ *
+ * Description:
+ *  Perform one step of decoding a sequence of words into binary data.
+ *
+ * Parameters:
+ *  index    - Index of word, e.g. return value of mn_next_word_index. Use
+ *             the value MN_EOF(=0) to signal the end of input.
+ *  dest     - Points to buffer to receive decoded binary result.
+ *  destsize - Size of buffer 
+ *  offset   - Pointer to an integer offset into the destination buffer for 
+ *             next data byte. Initialize *offset to 0 before first call to 
+ *             function. Modified by function and may be used as an 
+ * 	       indication for the amount of data actually decoded.
+ *
+ * Return value:
+ *  The return value indicates the status of the decoding function. It is
+ *  ok to ignore this value on all calls to the function except the last
+ *  one (with index=MN_EOF). Any errors encountered will be reported on. 
+ *  the last call. The error code is also returned in *offset (negative 
+ *  values indicate error).
+ *
+ * MN_OK (==0)	
+ *	for index!=MN_EOF a return value of MN_OK means that 
+ *	decoding has been successful so far.
+ *	for index==MN_EOF a return value of MN_OK means that decoding
+ *	of the entire buffer has been successful and the decoder is in
+ *	a valid state for the end of the message. A total of *offset
+ *	valid decoded bytes is in the buffer.
+ *  MN_EREM      
+ *	returned on MN_EOF when an unaccounted arithmetic remainder is
+ *	in the decoder. Most likely indicates a truncated word sequence.
+ *  MN_EOVERRUN	
+ *	Not enough room in buffer for decoded data.
+ *  MN_EOVERRUN24 
+ *	Returned when decoding of data is attempted after decoding one
+ *	of the 7 words reserved for 24 bit remainders at the end of the
+ *	message. Probably indicates a garbled messages.
+ *  MN_EINDEX	
+ *	Bad input index. Naturally this should not happen when using 
+ *	the result of mn_next_word_index.
+ *  MN_EINDEX24
+ *	Returned when one of the 7 words reserved for 24 bit remainders
+ *	is received at an offset inappropriate for a 24 bit remainder.
+ *  MN_EENCODING
+ *	Indicates an overflow in MN_BASE arithmetic. Approximately 0.09%
+ *	of the 3 word combinations are unused and will generate this error.
+ */
+
+int
+mn_decode_word_index (mn_index index, void *vdest, int destsize, int *offset)
+{
+  mn_word32 x;			/* Temporary for MN_BASE arithmetic */
+  int groupofs;
+  int i;
+  mn_byte *dest = vdest;
+
+  if (*offset < 0)		/* Error from previous call? report it */
+    return *offset;
+
+  if (index > MN_WORDS)	/* Word index out of range */
+    {
+      *offset = MN_EINDEX;
+      return *offset;
+    }
+
+  if (*offset > destsize)	/* out of range? */
+    {
+      *offset = MN_EOVERRUN;
+      return *offset;
+    }
+
+  if (index > MN_BASE && *offset % 4 != 2)
+    {				/* Unexpected 24 bit remainder word */
+      *offset = MN_EINDEX24;
+      return *offset;
+    }
+
+  groupofs = *offset & ~3;	/* Offset of 4 byte group containing offet */
+  x = 0;
+  for (i = 0; i < 4; i++)
+    if (groupofs + i < destsize)	/* Ignore any bytes outside buffer */
+      x |= (unsigned long)dest[groupofs + i] << (i * 8);	/* assemble number */
+
+  if (index == MN_EOF)		/* Got EOF signal */
+    {
+      switch (*offset % 4)
+	{
+	case 3:		/* group was three words and the last */
+	  return MN_OK;		/*  word was a 24 bit remainder */
+	case 2:		/* last group has two words */
+	  if (x <= 0xFFFF)	/*  should encode 16 bit data */
+	    return MN_OK;
+	  else
+	    {
+	      *offset = MN_EREM;
+	      return *offset;
+	    }
+	case 1:		/* last group has just one word */
+	  if (x <= 0xFF)	/*  should encode 8 bits */
+	    return MN_OK;
+	  else
+	    {
+	      *offset = MN_EREM;
+	      return *offset;
+	    }
+
+	case 0:		/* last group was full 3 words */
+	  return MN_OK;
+	}
+    }
+  if (*offset == destsize)	/* At EOF but didn't get MN_EOF */
+    {
+      *offset = MN_EOVERRUN;
+      return *offset;
+    }
+
+  index--;			/* 1 based to 0 based index */
+
+  switch (*offset % 4)
+    {
+    case 3:			/* Got data past 24 bit remainder */
+      *offset = MN_EOVERRUN24;
+      return *offset;
+    case 2:
+      if (index >= MN_BASE)
+	{			/* 24 bit remainder */
+	  x += (index - MN_BASE) * MN_BASE * MN_BASE;
+	  (*offset)++;		/* *offset%4 == 3 for next time */
+	}
+      else
+	{			/* catch invalid encodings */
+	  if (index >= 1625 || (index == 1624 && x > 1312671))
+	    {
+	      *offset = MN_EENCODING;
+	      return *offset;
+	    }
+	  x += index * MN_BASE * MN_BASE;
+	  (*offset) += 2;	/* *offset%4 == 0 for next time */
+	}
+      break;
+    case 1:
+      x += index * MN_BASE;
+      (*offset)++;
+      break;
+    case 0:
+      x = index;
+      (*offset)++;
+      break;
+    }
+
+  for (i = 0; i < 4; i++)
+    if (groupofs + i < destsize)	/* Don't step outside the buffer */
+      {
+	dest[groupofs + i] = (mn_byte) x % 256;
+	x /= 256;
+      }
+  return MN_OK;
+}
+
+/*
+ * mn_encode
+ *
+ * Description:
+ *  Encode a binary data buffer into a null-terminated sequence of words.
+ *  The word separators are taken from the format string. 
+ *
+ * Parameters:
+ *  src      - Pointer to the beginning of the binary data buffer.
+ *  srcsize  - Size in bytes of binary data buffer
+ *  dest     - Pointer to the beginning of a character buffer 
+ *  destsize - Size in characters of character buffer
+ *  format   - Null-terminated string describing the output format.
+ *             In the format string any letter or sequence of letters
+ *             acts as a placeholder for the encoded words. The word 
+ *             placeholders are separated by one or more non-letter
+ *             characters. When the encoder reaches the end of the 
+ *             format string it starts reading it again.
+ *             For sample formats see MN_F* constants in mnemonic.h
+ *	       If format is empty or NULL the format MN_FDEFAULT
+ *	       is used.
+ *
+ * Return value:
+ *  MN_OK(=0)
+ *	Encoding was successful.
+ *  MN_EOVERRUN
+ *	Output size exceeds size of destination buffer
+ *  MN_EFORMAT
+ *	Invalid format string. This function enforces formats which
+ *	will result in a string which can be successfully decoded by
+ *	the mn_decode function.
+ */
+
+int
+mn_encode (void *src, int srcsize, char *dest, int destsize, char *format)
+{
+  int n;
+  char *fmt;
+  char *destend = dest + destsize;
+  const char *word;
+
+  if (format == 0 || format[0] == '\0')
+    format = MN_FDEFAULT;
+  fmt = format;
+  for (n = 0; n < mn_words_required (srcsize); n++)
+    {
+      while (dest < destend && *fmt != '\0' && !isalpha (*fmt))
+	*dest++ = *fmt++;
+      if (dest >= destend)
+	return MN_EOVERRUN;
+      if (*fmt == '\0')
+	{
+	  if (isalpha (fmt[-1]) && isalpha (format[0]))
+	    return MN_EFORMAT;
+	  fmt = format;
+	  while (dest < destend && *fmt != '\0' && !isalpha (*fmt))
+	    *dest++ = *fmt++;
+	  if (!isalpha (*fmt))
+	    return MN_EFORMAT;
+	}
+      word = mn_encode_word (src, srcsize, n);
+      if (word == 0)
+	return MN_EOVERRUN;	/* shouldn't happen, actually */
+
+      while (isalpha (*fmt))
+	fmt++;
+      while (dest < destend && *word != '\0')
+	*dest++ = *word++;
+    }
+  if (dest < destend)
+    *dest++ = '\0';
+  else
+    return MN_EOVERRUN;
+  return MN_OK;
+}
+
+/*
+ * mn_decode
+ *
+ * Description:
+ *  Decode a text representation in null-terminated character buffer src to 
+ *  binary buffer dest.
+ *
+ * Parameters:
+ *  src      - Pointer to null-terminated character buffer 
+ *  dest     - Pointer to beginning of destination buffer
+ *  destsize - Size in bytes of destination buffer
+ *
+ * Return value:
+ *  This function may return all the value returned by mn_decode_word_index
+ *  plus the following result code:
+ *
+ * MN_EWORD  - Unrecognized word.
+ */
+
+int
+mn_decode (char *src, void *dest, int destsize)
+{
+  mn_index index;
+  int offset = 0;
+  int status;
+
+  while ((index = mn_next_word_index (&src)) != 0)
+    {
+      (void) mn_decode_word_index (index, dest, destsize, &offset);
+    }
+  if (*src != 0)
+    return MN_EWORD;
+  status = mn_decode_word_index (MN_EOF, dest, destsize, &offset);
+  if (status < 0)
+    return status;
+  return offset;
+}

+ 69 - 0
ctypes/mnemonicode/mnemonic.h

@@ -0,0 +1,69 @@
+/* mnemonic.h 
+
+ Copyright (c) 2000  Oren Tirosh <oren@hishome.net>
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in
+ all copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ THE SOFTWARE.
+
+*/
+
+#define MN_BASE		1626		/* cubic root of 2^32, rounded up */
+#define MN_REMAINDER	7		/* extra words for 24 bit remainders */
+#define MN_WORDS (MN_BASE+MN_REMAINDER)	/* total number of words */
+#define MN_WORD_BUFLEN	25		/* size for a word buffer+headroom */
+
+#define MN_EOF		0		/* signal end to mn_decode_word_index */
+
+/* result codes */
+#define MN_OK		0		/* decoding ok */
+#define MN_EREM		-1		/* unexpected arithmetic remainder */
+#define MN_EOVERRUN	-2		/* output buffer overrun */
+#define MN_EOVERRUN24	-3		/* data after 24 bit remainder */
+#define MN_EINDEX	-4		/* bad word index */
+#define MN_EINDEX24	-5		/* unexpected 24 bit remainder word */
+#define MN_EENCODING	-6		/* invalid arithmetic encoding */
+#define MN_EWORD	-7		/* unrecognized word */
+#define MN_EFORMAT	-8		/* bad format string */
+
+/* Sample formats for mn_encode */
+#define MN_FDEFAULT 		"x-x-x--"
+#define MN_F64BITSPERLINE	" x-x-x--x-x-x\n"
+#define MN_F96BITSPERLINE	" x-x-x--x-x-x--x-x-x\n"
+#define MN_F128BITSPERLINE	" x-x-x--x-x-x--x-x-x--x-x-x\n"
+/* Note that the last format does not fit in a standard 80 character line */
+
+typedef unsigned char mn_byte;		/* 8 bit quantity */
+typedef unsigned long mn_word32;	/* temporary value, at least 32 bits */
+/* Range checks assume that mn_index is unsigned (=> can't be <0).  --DV */
+typedef unsigned int mn_index;		/* index into wordlist */
+
+extern const char *mn_words[];		/* the word list itself */
+extern const char *mn_wordlist_version;	/* version notice string */
+
+int 		mn_decode (char *src, void *dest, int destsize);
+int 		mn_encode (void *src , int srcsize, 
+			   char *dest, int destsize, char *format);
+
+int 		mn_words_required (int size);
+mn_index 	mn_encode_word_index (void *src, int srcsize, int n);
+const char*	mn_encode_word (void *src, int srcsize, int n);
+mn_index 	mn_next_word_index (char **ptr);
+int 		mn_decode_word_index (mn_index index, void *dest, 
+				     int destsize, int *offset);
+
+

+ 90 - 0
ctypes/mnemonicode/mnencode.c

@@ -0,0 +1,90 @@
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <ctype.h>
+#include "mnemonic.h"
+
+int counthex(const mn_byte *s, size_t len) {
+	int total = 0;
+	size_t i;
+	for(i = 0; i < len; i++) {
+		if(isxdigit(s[i])) total++;
+	}
+	return total;
+}
+
+int countLeadingCrapAndZeros(const mn_byte *in, size_t inlen) {
+	size_t i;
+	for(i = 0; i < inlen; i++) {
+		if(in[i] != '0' && isxdigit(in[i])) break; /* Find first non-zero hex digit */
+	}
+	return i;
+}
+
+int hex2bytes(int inlen, const mn_byte *in, mn_byte *out) {
+	char t[3];
+	int i, j = 0, k = 0;
+	i = countLeadingCrapAndZeros(in, inlen);
+	inlen = inlen - i;
+	in = in + i;
+	if(counthex(in, inlen) % 2 != 0) {
+		t[j++] = '0';
+	}
+	for(i = 0; i < inlen; i++) {
+		if(isxdigit(in[i])) {
+			t[j++] = in[i];
+			if(j == 2) {
+				t[2] = '\0';
+				errno = 0;
+				out[k++] = strtol(t, NULL, 16);
+				if(errno != 0) {
+					return -1;
+				}
+				j = 0;
+			}
+		}
+	}
+	return k;
+}
+
+int main(int argc, char **argv) {
+	mn_byte *buf;
+	mn_byte cbuf[0x10000];
+	mn_byte xbuf[0x8000];
+	char outbuf[0x90000];
+	int buflen;
+	int n;
+	int hex_encoded_input = 0;
+
+	if(argc > 1 && strcmp(argv[1],"-x") == 0) {
+		hex_encoded_input = 1;
+	}
+
+	fprintf (stderr, "%s\n", mn_wordlist_version);
+
+	buflen = fread(cbuf, 1, sizeof cbuf, stdin);
+
+	if(hex_encoded_input) {
+		buflen = hex2bytes(buflen, cbuf, xbuf);
+		if(buflen < 0) {
+			perror("hex2bytes");
+			return EXIT_FAILURE;
+		}
+		buf = xbuf;
+	} else {
+		buf = cbuf;
+	}
+
+	n = mn_encode(buf, buflen, outbuf, sizeof outbuf, "x x x. x x x. x x x\n");
+
+	if(n != 0) {
+		fprintf(stderr, "mn_encode error %d\n", n);
+		return EXIT_FAILURE;
+	}
+
+	fwrite(outbuf, 1, strlen (outbuf), stdout);
+	putchar('\n');
+
+	return EXIT_SUCCESS;
+}

+ 17 - 0
ctypes/pyproject.toml

@@ -0,0 +1,17 @@
+[project]
+name = "mnemonic-api"
+version = "0.1.0"
+description = "Add your description here"
+readme = "README.md"
+authors = [
+    { name = "ZiB", email = "zib@zi3.xyz" }
+]
+requires-python = ">=3.11"
+dependencies = [
+    "fastapi>=0.137.0",
+    "uvicorn[standard]>=0.49.0",
+]
+
+[build-system]
+requires = ["uv_build>=0.11.19,<0.12.0"]
+build-backend = "uv_build"

+ 2 - 0
ctypes/src/mnemonic_api/__init__.py

@@ -0,0 +1,2 @@
+def hello() -> str:
+    return "Hello from mnemonic-api!"

+ 40 - 0
ctypes/src/mnemonic_api/app.py

@@ -0,0 +1,40 @@
+import os
+
+from fastapi import FastAPI, HTTPException, Query, Request, Response
+
+from .lib import decode, encode
+
+app = FastAPI(title="mnemonic-api")
+
+
+@app.post("/encode")
+async def encode_endpoint(request: Request) -> Response:
+  data = await request.body()
+  if not data:
+    raise HTTPException(400, "request body must not be empty")
+  if len(data) > 1000:
+    raise HTTPException(413, "input exceeds 1000 bytes")
+  try:
+    return Response(content=encode(data), media_type="text/plain")
+  except ValueError as exc:
+    raise HTTPException(422, str(exc)) from exc
+
+
+@app.post("/decode")
+async def decode_endpoint(request: Request) -> Response:
+  body = await request.body()
+  try:
+    text = body.decode("ascii")
+  except UnicodeDecodeError as exc:
+    raise HTTPException(400, "body must be ASCII text") from exc
+  try:
+    return Response(content=decode(text), media_type="application/octet-stream")
+  except ValueError as exc:
+    raise HTTPException(422, str(exc)) from exc
+
+
+@app.get("/words")
+async def words_endpoint(length: int = Query(..., gt=0, le=4096)) -> Response:
+  nbytes = (length * 4) // 3
+  data = os.urandom(nbytes)
+  return Response(content=encode(data), media_type="text/plain")

+ 44 - 0
ctypes/src/mnemonic_api/lib.py

@@ -0,0 +1,44 @@
+import ctypes
+import os
+
+_lib = ctypes.CDLL(os.environ.get("LIBMNEMONIC_PATH", "./libmnemonic.so"))
+
+_lib.mn_words_required.restype = ctypes.c_int
+_lib.mn_words_required.argtypes = [ctypes.c_int]
+
+_lib.mn_encode.restype = ctypes.c_int
+_lib.mn_encode.argtypes = [
+  ctypes.c_void_p, ctypes.c_int,
+  ctypes.c_char_p, ctypes.c_int,
+  ctypes.c_char_p,
+]
+
+_lib.mn_decode.restype = ctypes.c_int
+_lib.mn_decode.argtypes = [ctypes.c_char_p, ctypes.c_void_p, ctypes.c_int]
+
+_FORMAT = b"x x x. x x x. x x x\n"
+_MAX_WORD_LEN = 7
+
+
+def encode(data: bytes) -> str:
+  srcsize = len(data)
+  nwords = _lib.mn_words_required(srcsize)
+  destsize = nwords * (_MAX_WORD_LEN + 3) + 2
+  dest = ctypes.create_string_buffer(destsize)
+  rc = _lib.mn_encode(
+    ctypes.cast(ctypes.c_char_p(data), ctypes.c_void_p),
+    srcsize, dest, destsize, _FORMAT,
+  )
+  if rc != 0:
+    raise ValueError(f"mn_encode returned {rc}")
+  return dest.value.decode("ascii")
+
+
+def decode(text: str) -> bytes:
+  src = text.encode("ascii")
+  destsize = max(len(src), 4)
+  dest = ctypes.create_string_buffer(destsize)
+  rc = _lib.mn_decode(src, dest, destsize)
+  if rc < 0:
+    raise ValueError(f"mn_decode returned {rc}")
+  return bytes(dest.raw[:rc])

+ 0 - 0
ctypes/src/mnemonic_api/py.typed


+ 587 - 0
ctypes/uv.lock

@@ -0,0 +1,587 @@
+version = 1
+revision = 3
+requires-python = ">=3.11"
+
+[[package]]
+name = "annotated-doc"
+version = "0.0.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" },
+]
+
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
+]
+
+[[package]]
+name = "anyio"
+version = "4.13.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "idna" },
+    { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "fastapi"
+version = "0.137.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "annotated-doc" },
+    { name = "pydantic" },
+    { name = "starlette" },
+    { name = "typing-extensions" },
+    { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/da/fe/fb25c287ff7e0f79fc6acf2e8b812725dad28d2a1446c0410bab1422ac90/fastapi-0.137.0.tar.gz", hash = "sha256:d0565d551f65a803ecff245390840867186f456ef98971f750724eed16e1541c", size = 408023, upload-time = "2026-06-14T12:51:30.672Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/e7/f1/b38481428e50131e5345b535414d11d196f14990122fe69c9020c64e5683/fastapi-0.137.0-py3-none-any.whl", hash = "sha256:6dcbde8d464f92117c1accb9e42720f8e423fa9b86cb563b1f5862f785a06498", size = 121777, upload-time = "2026-06-14T12:51:29.067Z" },
+]
+
+[[package]]
+name = "h11"
+version = "0.16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" },
+]
+
+[[package]]
+name = "httptools"
+version = "0.8.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/43/e5/d471fcb0e14523fe1c3f4ba58ca52480e7bd70ad7109a3846bc75892f7fb/httptools-0.8.0.tar.gz", hash = "sha256:6b2a32f18d97e16e90827d7a819ffa8dbd8cc245fc4e1fa9d1095b54ef4bd999", size = 271342, upload-time = "2026-05-25T22:17:48.841Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/f8/d2/c3eedaef57de65c3cc5f8dc244cf12d09c84ad258a479055aad6db23206c/httptools-0.8.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ed377e64805bdba4943c82717333f8f8603a13b09aff9cead2717c6c817fb168", size = 208428, upload-time = "2026-05-25T22:16:59.717Z" },
+    { url = "https://files.pythonhosted.org/packages/f1/94/dfe435d90d0ef61ec0f2cc3d480eef78c59727c6c2ce039f433882f6131a/httptools-0.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9518c406d7b310f05adb1a37f80acabac40504a575d7c0da6d3e365c695ac20d", size = 113366, upload-time = "2026-05-25T22:17:00.795Z" },
+    { url = "https://files.pythonhosted.org/packages/cc/d4/13025f1a56e615dcb331e0bbe2d9a1143212b58c263385fc5d2e558f5bac/httptools-0.8.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:57278e6fa0424c42a8a3e454828ab4f0aff27b40cddf9679579b98c6dce6a376", size = 464676, upload-time = "2026-05-25T22:17:02.014Z" },
+    { url = "https://files.pythonhosted.org/packages/bf/95/4c1c26c0b985f8a3331682d802598f14e32dc41bf7509266eb2c04ad4801/httptools-0.8.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbb8caadb2b742d293169d2b458b5c001ef70e3158704aa3d3ef9597624c5d1d", size = 464235, upload-time = "2026-05-25T22:17:03.109Z" },
+    { url = "https://files.pythonhosted.org/packages/a2/82/6735be2b0ca527718c431cdb8e5f70c3862c0844a687df0f572c51e11497/httptools-0.8.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:52dd695b865fe96d9d2b16b64a895f3f57bf3cb064e8383cd3b5713a069e8085", size = 449809, upload-time = "2026-05-25T22:17:04.443Z" },
+    { url = "https://files.pythonhosted.org/packages/b5/f9/5811c74f37a758c8a4aa3dc430375119d335947e883efc4664d8f3559a41/httptools-0.8.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:20b4aac66ff65f7db06a375808b78f42a94970aa22e826b3cb2b43eb09174124", size = 452174, upload-time = "2026-05-25T22:17:05.476Z" },
+    { url = "https://files.pythonhosted.org/packages/cc/94/97b75870dea07b71e3ec535cebe525b08d723152e4c7d13fa887e51f4de2/httptools-0.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:a1b4c8e7a489a0d750d91894e9a8cdc295838f1924c0ca903ae993456fddec07", size = 90991, upload-time = "2026-05-25T22:17:06.75Z" },
+    { url = "https://files.pythonhosted.org/packages/14/88/1d21a36da8f5cb0fa49eafd4b169eba5608d57e75bbcf61845cbc6243216/httptools-0.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:880490234c10f70a9830743097e8958d6e4b9f5a0ffc24515023afeef984054d", size = 208247, upload-time = "2026-05-25T22:17:07.843Z" },
+    { url = "https://files.pythonhosted.org/packages/a5/42/cc4feea2945cb3051038f090c9b36bd5b8a9d7f5a894a506a8983e33fd1c/httptools-0.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5931891fb7b441b8a3853cf1b85c82c903defce084dd5f6771ca46e31bf862c5", size = 113064, upload-time = "2026-05-25T22:17:09.136Z" },
+    { url = "https://files.pythonhosted.org/packages/e3/a6/febbb8b8db0f58b38e44ad6cb946e6a255ae49b55f2e8543408fb7501ccd/httptools-0.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b15fc622b0f869d19207c4089a501d9bcc63ca5e071ffdd2f03f922df882dcb2", size = 523851, upload-time = "2026-05-25T22:17:10.106Z" },
+    { url = "https://files.pythonhosted.org/packages/b7/e4/f90a0df0b83beff265b7e3b65f2a4cefd95792d4be0ac3e16049f2acd3c2/httptools-0.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:425f83884fd6343828d8c565f046cb72b6d19063f6924093e11bcd8e1548cd09", size = 518842, upload-time = "2026-05-25T22:17:11.218Z" },
+    { url = "https://files.pythonhosted.org/packages/9e/2d/0c9ac76dd2c893841fbf6498d6acec4f2442e1b7067f6e3e316a80e494e8/httptools-0.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7c3c97f4311c7be57e2986629df89d49cb434dbff78eafcd48c2bff986b15a", size = 501238, upload-time = "2026-05-25T22:17:12.728Z" },
+    { url = "https://files.pythonhosted.org/packages/ca/42/906adc91ae3a5fa9c59c0a2f21c139725bd7e5b41ae6acd485cd14123ebf/httptools-0.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a1afd7c9fbff0d9f5d489c4ce2768bd09c84a46ddefc7161e6aa82ae35c85745", size = 509567, upload-time = "2026-05-25T22:17:13.842Z" },
+    { url = "https://files.pythonhosted.org/packages/05/0b/4240efeb672751ee5b9b380cb0e3fdc050bc05f68adc7a8aefc4fcd9a69a/httptools-0.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd96f29b4bab1d42fa6e3d008711c75e0f79e94e06827330160e3a304227f150", size = 90918, upload-time = "2026-05-25T22:17:15.155Z" },
+    { url = "https://files.pythonhosted.org/packages/5e/e5/8cfcabc5546e8022f168be28bcdaa128a240a0befdd03b59d558b4f18bd6/httptools-0.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:614ceea8ea606848bece2338ac03b3ce5324bcb4be8dc7d377ed708012fa4db8", size = 205148, upload-time = "2026-05-25T22:17:16.333Z" },
+    { url = "https://files.pythonhosted.org/packages/2a/0e/0fb14848c19a686c8062ff9067c1a48793e3224b47bc5b201535b6036fce/httptools-0.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2d689918c15a013c65ef52d9fd495d766893ab831a2c8d89f2ac5940a5df847c", size = 111368, upload-time = "2026-05-25T22:17:17.586Z" },
+    { url = "https://files.pythonhosted.org/packages/2e/1b/46f1cecf06b9bbde8e4b8c88034ac7908989e5ff7a3a388ef38392949c1f/httptools-0.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb3028cca2fc0a6d720e52ef61d8ebb62fcbfeb1de56874546d858d3f25a26b7", size = 486447, upload-time = "2026-05-25T22:17:18.564Z" },
+    { url = "https://files.pythonhosted.org/packages/77/00/258bfc0837221f81d9725c45f9b948a6a6b2994a147a4fb66e85100c668f/httptools-0.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:88bdd940f2b5d487b4d032c6afa5489a7dc4694410d43de3c38c4fb3af0dc45d", size = 482448, upload-time = "2026-05-25T22:17:19.912Z" },
+    { url = "https://files.pythonhosted.org/packages/04/ab/d1cef3b5523f4d272a70f42a776c3169a2dddfe3a54de4b2ce4a36341528/httptools-0.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a43c9dd399758ccc0531acb0a3c4a6c299ee893ee9400e9c893b7bdcfae0681", size = 464460, upload-time = "2026-05-25T22:17:20.882Z" },
+    { url = "https://files.pythonhosted.org/packages/ce/48/5d1d072442277bb2b3434e0e60690b8e8c23840ef7de8b6ea54040a536d3/httptools-0.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0770728beb05094c809b98e814edff5fef69d26ad7d21185f2f6d5884a0ba683", size = 471312, upload-time = "2026-05-25T22:17:22.085Z" },
+    { url = "https://files.pythonhosted.org/packages/0d/66/b96623b27e51a68199ef4efdda0613cced9233fe3062ac74e50749c5ad37/httptools-0.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:7685df791fad561384bfb139e77fde27a1ffd93134e016f95a0db424ffbf77b1", size = 90117, upload-time = "2026-05-25T22:17:23.074Z" },
+    { url = "https://files.pythonhosted.org/packages/1a/12/fa3fbf5f9517b273edea2dc982aa82a8c634091e67c590792b729017bc6f/httptools-0.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:de242a49b5d18e0a8776e654e9f6bf6d89f3875a5c35b425a0e7ce940feb3fd6", size = 206183, upload-time = "2026-05-25T22:17:24.004Z" },
+    { url = "https://files.pythonhosted.org/packages/30/fc/5e7c4cb443370f2090a3aba0453a07384d29ff66b7435bb90e77e1037599/httptools-0.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:159e9ab5f701ccd42e555a12f1ad8ff69702910fc1c996cf2bb66e5fcb7a231b", size = 112079, upload-time = "2026-05-25T22:17:25.216Z" },
+    { url = "https://files.pythonhosted.org/packages/ba/53/771bd891eb0f236f32145d6a1775777ec85745f3cc983a1f23d1a3b8ddfe/httptools-0.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c4a9f1707e4823d54dfec6c33fa3697d302aed536ed352a7ebb5a061ddb869d0", size = 481596, upload-time = "2026-05-25T22:17:26.186Z" },
+    { url = "https://files.pythonhosted.org/packages/62/42/94e15bc68ce3d423243c45d7f1b0c7561f13844f97dc52ae23182fb65628/httptools-0.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d76ad7b951387e3632c8716a9bb03ac5b45c5f16119aa409db0459520887944e", size = 480865, upload-time = "2026-05-25T22:17:27.542Z" },
+    { url = "https://files.pythonhosted.org/packages/1c/7c/fe2980fc03723272e30f135b62360b075f513dfe7cc73aef36c7f04012bd/httptools-0.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a3b7387147361c3fd47a0bde763c5c91b5b4cd4dc9989b8ece84ff436c99843b", size = 463189, upload-time = "2026-05-25T22:17:28.546Z" },
+    { url = "https://files.pythonhosted.org/packages/15/1b/47fc5fff68acd1bfa20b4734059c9a06cadb88119dcd5258b5b0d21d91c8/httptools-0.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f256d6ce930c52ca1cb2a960b7da03548c454e7d28b06059ad41bfe789036ce0", size = 466610, upload-time = "2026-05-25T22:17:29.816Z" },
+    { url = "https://files.pythonhosted.org/packages/60/bd/07b13c93ffd9bec9546e0d43f8e19378dd696dbd278511406bc07371ef1f/httptools-0.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:19d1ee275bb59ba2643ba9a3a1e51cc0c788caf2b8df506368e03f56fdd08527", size = 92705, upload-time = "2026-05-25T22:17:31.133Z" },
+    { url = "https://files.pythonhosted.org/packages/fd/c4/121648f68ce066d7bd762d6b6d97e620847642d38d54f3d90ff11d947629/httptools-0.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:de1ed58a974e75d56560acc7e7fed01a454994429456f65209789992e41f2568", size = 215023, upload-time = "2026-05-25T22:17:32.401Z" },
+    { url = "https://files.pythonhosted.org/packages/b9/b0/312a062ae741ae3e8baa8c8bf20be81b2e67337b259ab4349bebc7b6142e/httptools-0.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e93c227b595c6926c1acee96891dd9da4be338cfbe82e5cd3bb9d8dd7dc4ac0b", size = 117405, upload-time = "2026-05-25T22:17:33.742Z" },
+    { url = "https://files.pythonhosted.org/packages/fc/37/fccd705f795386bb05bf413012fecff2a33e5aa8c2f069096de3e9fd8702/httptools-0.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2a021c3a8e65cc125390d72f59b968afca3bdcaff25bd67965e0a055a14946ca", size = 558497, upload-time = "2026-05-25T22:17:34.732Z" },
+    { url = "https://files.pythonhosted.org/packages/bd/39/f172e8003576de35f5ba77ff417cf0e34429d35dc014deef15afa337a72c/httptools-0.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48774d39cbb70e2b1f71f88852a3087ae1d3a1eb80482bb48c13067ab080c14f", size = 571585, upload-time = "2026-05-25T22:17:35.813Z" },
+    { url = "https://files.pythonhosted.org/packages/3e/b9/f5564760af99f3dbbf3f9104dc00e5da27e96cf433c6bdcf77617f70bf3f/httptools-0.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:88eead8ec8680a9f146c655bc88445a325bd7921cfd8194c7337e9467282427d", size = 543297, upload-time = "2026-05-25T22:17:37.08Z" },
+    { url = "https://files.pythonhosted.org/packages/99/67/8d9f2c313618e161b82f3873188e7196126da1d6e29688df40eb3997c77a/httptools-0.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c032fa028f46871ec7e1fc59fc15e8023eab3e6bbe6ece786a1611719a5d081", size = 539535, upload-time = "2026-05-25T22:17:38.032Z" },
+    { url = "https://files.pythonhosted.org/packages/48/63/b906c01e53f50d432c0defe43ce52764a111dc1bdd028bafbeb54dcfd008/httptools-0.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:384c17174464c8e873398b7af24f0b1f44d992c820328413951a625323155d77", size = 108209, upload-time = "2026-05-25T22:17:39.473Z" },
+]
+
+[[package]]
+name = "idna"
+version = "3.18"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
+]
+
+[[package]]
+name = "mnemonic-api"
+version = "0.1.0"
+source = { editable = "." }
+dependencies = [
+    { name = "fastapi" },
+    { name = "uvicorn", extra = ["standard"] },
+]
+
+[package.metadata]
+requires-dist = [
+    { name = "fastapi", specifier = ">=0.137.0" },
+    { name = "uvicorn", extras = ["standard"], specifier = ">=0.49.0" },
+]
+
+[[package]]
+name = "pydantic"
+version = "2.13.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "annotated-types" },
+    { name = "pydantic-core" },
+    { name = "typing-extensions" },
+    { name = "typing-inspection" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
+]
+
+[[package]]
+name = "pydantic-core"
+version = "2.46.4"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" },
+    { url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" },
+    { url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" },
+    { url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" },
+    { url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" },
+    { url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" },
+    { url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" },
+    { url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" },
+    { url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" },
+    { url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" },
+    { url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" },
+    { url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" },
+    { url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" },
+    { url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" },
+    { url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" },
+    { url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" },
+    { url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" },
+    { url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" },
+    { url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" },
+    { url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" },
+    { url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" },
+    { url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" },
+    { url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" },
+    { url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" },
+    { url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" },
+    { url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" },
+    { url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" },
+    { url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" },
+    { url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" },
+    { url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" },
+    { url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
+    { url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
+    { url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
+    { url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
+    { url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
+    { url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
+    { url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
+    { url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
+    { url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
+    { url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
+    { url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
+    { url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
+    { url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
+    { url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
+    { url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
+    { url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
+    { url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
+    { url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
+    { url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
+    { url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
+    { url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
+    { url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
+    { url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
+    { url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
+    { url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
+    { url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
+    { url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
+    { url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
+    { url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
+    { url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
+    { url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
+    { url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
+    { url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
+    { url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
+    { url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
+    { url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
+    { url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
+    { url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
+    { url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
+    { url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
+    { url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
+    { url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
+    { url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
+    { url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
+    { url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
+    { url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" },
+    { url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" },
+    { url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" },
+    { url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" },
+    { url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" },
+    { url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" },
+    { url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" },
+    { url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" },
+    { url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" },
+    { url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" },
+    { url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" },
+    { url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" },
+    { url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" },
+    { url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" },
+    { url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" },
+    { url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.2.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" },
+]
+
+[[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 = "starlette"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "anyio" },
+    { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]
+
+[[package]]
+name = "typing-inspection"
+version = "0.4.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
+]
+
+[[package]]
+name = "uvicorn"
+version = "0.49.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "click" },
+    { name = "h11" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284, upload-time = "2026-06-03T22:01:30.448Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f", size = 71376, upload-time = "2026-06-03T22:01:29.037Z" },
+]
+
+[package.optional-dependencies]
+standard = [
+    { name = "colorama", marker = "sys_platform == 'win32'" },
+    { name = "httptools" },
+    { name = "python-dotenv" },
+    { name = "pyyaml" },
+    { name = "uvloop", marker = "platform_python_implementation != 'PyPy' and sys_platform != 'cygwin' and sys_platform != 'win32'" },
+    { name = "watchfiles" },
+    { name = "websockets" },
+]
+
+[[package]]
+name = "uvloop"
+version = "0.22.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" },
+    { url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" },
+    { url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" },
+    { url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" },
+    { url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" },
+    { url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" },
+    { url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" },
+    { url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" },
+    { url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" },
+    { url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" },
+    { url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" },
+    { url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" },
+    { url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" },
+    { url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" },
+    { url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" },
+    { url = "https://files.pythonhosted.org/packages/15/c0/0be24758891ef825f2065cd5db8741aaddabe3e248ee6acc5e8a80f04005/uvloop-0.22.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0530a5fbad9c9e4ee3f2b33b148c6a64d47bbad8000ea63704fa8260f4cf728e", size = 4366890, upload-time = "2025-10-16T22:16:40.547Z" },
+    { url = "https://files.pythonhosted.org/packages/d2/53/8369e5219a5855869bcee5f4d317f6da0e2c669aecf0ef7d371e3d084449/uvloop-0.22.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bc5ef13bbc10b5335792360623cc378d52d7e62c2de64660616478c32cd0598e", size = 4119472, upload-time = "2025-10-16T22:16:41.694Z" },
+    { url = "https://files.pythonhosted.org/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" },
+    { url = "https://files.pythonhosted.org/packages/90/cd/b62bdeaa429758aee8de8b00ac0dd26593a9de93d302bff3d21439e9791d/uvloop-0.22.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3879b88423ec7e97cd4eba2a443aa26ed4e59b45e6b76aabf13fe2f27023a142", size = 1362067, upload-time = "2025-10-16T22:16:44.503Z" },
+    { url = "https://files.pythonhosted.org/packages/0d/f8/a132124dfda0777e489ca86732e85e69afcd1ff7686647000050ba670689/uvloop-0.22.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4baa86acedf1d62115c1dc6ad1e17134476688f08c6efd8a2ab076e815665c74", size = 752423, upload-time = "2025-10-16T22:16:45.968Z" },
+    { url = "https://files.pythonhosted.org/packages/a3/94/94af78c156f88da4b3a733773ad5ba0b164393e357cc4bd0ab2e2677a7d6/uvloop-0.22.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:297c27d8003520596236bdb2335e6b3f649480bd09e00d1e3a99144b691d2a35", size = 4272437, upload-time = "2025-10-16T22:16:47.451Z" },
+    { url = "https://files.pythonhosted.org/packages/b5/35/60249e9fd07b32c665192cec7af29e06c7cd96fa1d08b84f012a56a0b38e/uvloop-0.22.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c1955d5a1dd43198244d47664a5858082a3239766a839b2102a269aaff7a4e25", size = 4292101, upload-time = "2025-10-16T22:16:49.318Z" },
+    { url = "https://files.pythonhosted.org/packages/02/62/67d382dfcb25d0a98ce73c11ed1a6fba5037a1a1d533dcbb7cab033a2636/uvloop-0.22.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b31dc2fccbd42adc73bc4e7cdbae4fc5086cf378979e53ca5d0301838c5682c6", size = 4114158, upload-time = "2025-10-16T22:16:50.517Z" },
+    { url = "https://files.pythonhosted.org/packages/f0/7a/f1171b4a882a5d13c8b7576f348acfe6074d72eaf52cccef752f748d4a9f/uvloop-0.22.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:93f617675b2d03af4e72a5333ef89450dfaa5321303ede6e67ba9c9d26878079", size = 4177360, upload-time = "2025-10-16T22:16:52.646Z" },
+    { url = "https://files.pythonhosted.org/packages/79/7b/b01414f31546caf0919da80ad57cbfe24c56b151d12af68cee1b04922ca8/uvloop-0.22.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:37554f70528f60cad66945b885eb01f1bb514f132d92b6eeed1c90fd54ed6289", size = 1454790, upload-time = "2025-10-16T22:16:54.355Z" },
+    { url = "https://files.pythonhosted.org/packages/d4/31/0bb232318dd838cad3fa8fb0c68c8b40e1145b32025581975e18b11fab40/uvloop-0.22.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:b76324e2dc033a0b2f435f33eb88ff9913c156ef78e153fb210e03c13da746b3", size = 796783, upload-time = "2025-10-16T22:16:55.906Z" },
+    { url = "https://files.pythonhosted.org/packages/42/38/c9b09f3271a7a723a5de69f8e237ab8e7803183131bc57c890db0b6bb872/uvloop-0.22.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:badb4d8e58ee08dad957002027830d5c3b06aea446a6a3744483c2b3b745345c", size = 4647548, upload-time = "2025-10-16T22:16:57.008Z" },
+    { url = "https://files.pythonhosted.org/packages/c1/37/945b4ca0ac27e3dc4952642d4c900edd030b3da6c9634875af6e13ae80e5/uvloop-0.22.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b91328c72635f6f9e0282e4a57da7470c7350ab1c9f48546c0f2866205349d21", size = 4467065, upload-time = "2025-10-16T22:16:58.206Z" },
+    { url = "https://files.pythonhosted.org/packages/97/cc/48d232f33d60e2e2e0b42f4e73455b146b76ebe216487e862700457fbf3c/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:daf620c2995d193449393d6c62131b3fbd40a63bf7b307a1527856ace637fe88", size = 4328384, upload-time = "2025-10-16T22:16:59.36Z" },
+    { url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" },
+]
+
+[[package]]
+name = "watchfiles"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "anyio" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/cd/41/5e1a4bb12aac5f1493fa1bdc11154eca3b258ca4eba65d39c473fe19d8e9/watchfiles-1.2.0.tar.gz", hash = "sha256:c995fba777f1ea992f090f9236e9284cf7a5d1a0130dd5a3d82c598cacd76838", size = 108252, upload-time = "2026-05-18T04:32:04.251Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/fc/3d/8024c801df84d1587740d0359e7fdd80afeae3d159011f3d5376dd82f18e/watchfiles-1.2.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:704fd259e332e01f9b9c178f4bce9e49027e5587cc2600eeeaf8e76e1c846201", size = 400242, upload-time = "2026-05-18T04:31:19.014Z" },
+    { url = "https://files.pythonhosted.org/packages/87/5b/f4dfd45323e949984a3a7f9dc31d1cbb049921e7d98253488dda72ccdaa9/watchfiles-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6543cf55d170003296d185c0af981f3e1311564907e1f4e08671fc7693a890a5", size = 394562, upload-time = "2026-05-18T04:30:08.46Z" },
+    { url = "https://files.pythonhosted.org/packages/98/d8/19483ef075d601c409bce8bcbb5c0f81a10876fff870400568f08ce484a1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:89d8c2394a065ca86f5d2910ff263ae67c127e1376ccc4f9fc35c71db879f80a", size = 456611, upload-time = "2026-05-18T04:30:45.723Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/6a/cc81fbe7ee42f2f22e661a6e12def7807e01b14b2f39e0ff83fd373fd307/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:772b80df316480d894a0e3165fdd19cf77f5d17f9a787f94029465ad0e3529d1", size = 461379, upload-time = "2026-05-18T04:31:29.292Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/57/7e669002082c0a0f4fb5113bb70125f7110124b846b0a11bc5ae8e90eac1/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d158cd89df6053823533e06fb1d73c549133bff5f0396170c0e53d9559340717", size = 493556, upload-time = "2026-05-18T04:30:05.44Z" },
+    { url = "https://files.pythonhosted.org/packages/45/7d/f60a2b19807b21fe8281f3a8da4f59eef0d5f96825ac4680ba2d4f2ebf91/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d516b3283a758e087841aedb8031549fb41ced08f3db10aa6d2bf32dc042525b", size = 575255, upload-time = "2026-05-18T04:30:40.568Z" },
+    { url = "https://files.pythonhosted.org/packages/bd/49/77f5b5e6efbcd57482f74948ebb1b97e5c0046d6b61475042d830c84b3ff/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:53b2290c92e0506d102cd448fbc610d87079553f86caa39d67440856a8b8bba5", size = 467052, upload-time = "2026-05-18T04:31:17.942Z" },
+    { url = "https://files.pythonhosted.org/packages/ee/5a/73e2959af1b97fd5d556f9a8bdba017be23ceeef731869d5eaa0a753d5a3/watchfiles-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a711b51aec4370d0dcda5b6c09463206f133a5759341d7744b953a7b62e1100e", size = 456858, upload-time = "2026-05-18T04:30:30.182Z" },
+    { url = "https://files.pythonhosted.org/packages/50/57/1bc8c27fad7e6c19bddee15d276dbb6ab72480ec01c127afff1673aee417/watchfiles-1.2.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:e2ca07fa7d89195ec0865d3d285666286740bfa83d83e5cee204043a31ecc165", size = 467579, upload-time = "2026-05-18T04:32:15.897Z" },
+    { url = "https://files.pythonhosted.org/packages/09/6c/3c2e44edba3553c5e3c3b8c8a2a6dee6b9e12ae2cf4bd2378bebf9dc3038/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e0618518f282c4ebff60f5e5b1247b6d91bb8b9f4476947563a1e74acc66f3c6", size = 633253, upload-time = "2026-05-18T04:31:37.123Z" },
+    { url = "https://files.pythonhosted.org/packages/30/c2/d8c84a882ab39bbefcc4915ab3e91830b7a7e990c5570b0b69075aba3faf/watchfiles-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0d191c054d0715c3c95c99df9b8dbf6fd096d8c1e021e8f212e1bd8bc444ccb5", size = 660713, upload-time = "2026-05-18T04:31:24.62Z" },
+    { url = "https://files.pythonhosted.org/packages/a9/07/f97736a5fc605364fe67b25e9fa4a6965dfd4840d50c406ada507e9d735f/watchfiles-1.2.0-cp311-cp311-win32.whl", hash = "sha256:9342472aff9b093c5acd4f6d8f70ae0937964ab56542502bcf5579782da69ae8", size = 277222, upload-time = "2026-05-18T04:31:21.131Z" },
+    { url = "https://files.pythonhosted.org/packages/cf/99/2b04981977fc2608afd60360d928c6aecf6b950292ca221d98f4005f6694/watchfiles-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:dbd6c97045dad81227c8d040173da044c1de08de64a5ea8b555da4aee1d5fa22", size = 290274, upload-time = "2026-05-18T04:31:45.966Z" },
+    { url = "https://files.pythonhosted.org/packages/3c/74/f7f58a7075ee9cf612b0cfcddb78b8cd8234f0742d6f0075cf0da2dde1c6/watchfiles-1.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:57a2d9fa4fb4c2ecae57b13dfff2c7ab53e21a2ba674fe9f05506680fcdcc0d7", size = 283460, upload-time = "2026-05-18T04:31:39.126Z" },
+    { url = "https://files.pythonhosted.org/packages/b8/2f/e42c992d2afda3108ea1c02acecc991b9f31d05c14adc2a7cee9ee211fc4/watchfiles-1.2.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:bc13eb17538be00c874699dc0abe4ee2bc8d50bb1166a6b9e175ef3fd7eb8f26", size = 400115, upload-time = "2026-05-18T04:32:02.06Z" },
+    { url = "https://files.pythonhosted.org/packages/5f/8f/6af2ea19065c91d8b0ea3516fdfc8c0d349f407e8e9fbf4e5a17360de8ad/watchfiles-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2d95ddc1eb6914154253d239089900813f6a767e174b8e6a50e7fdacb7e4236c", size = 393659, upload-time = "2026-05-18T04:30:50.951Z" },
+    { url = "https://files.pythonhosted.org/packages/13/01/b32a967c56fb3e3e5be3db52c3d3b87fa4513aa367d8ed1ad96d42952e5f/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f70d8b291ef6e88d19b1f297a6905ddb978888d9272b0d05e6f53309856bcfc", size = 453207, upload-time = "2026-05-18T04:31:04.231Z" },
+    { url = "https://files.pythonhosted.org/packages/04/98/97557a812180338cb1abd32e1cffcc4588f59b5f23e0cb006b2ba95ba64a/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56d8641cf834c2836922899105bd3ce3d0dfc69291d52edf0b4d0436829b34c0", size = 459273, upload-time = "2026-05-18T04:31:50.377Z" },
+    { url = "https://files.pythonhosted.org/packages/e8/a8/b4b08dcb7653b8087c6586f7ce649505900e866bbcfe40dc9587af02e686/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2581a94056e55d7d0a31a823ea92bf73749c489ca2285bfdc0fbe6b2bb49d50c", size = 489927, upload-time = "2026-05-18T04:31:42.485Z" },
+    { url = "https://files.pythonhosted.org/packages/50/94/3dceea03545d2e5ddfd839f0ddd5e1cecbf1697b5a428d5ba11cef6af95d/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:41bc1199f7523b3f82843c88cbb979180c949caef0342cf90968f178e5d49b01", size = 570476, upload-time = "2026-05-18T04:31:03.071Z" },
+    { url = "https://files.pythonhosted.org/packages/cc/f2/d39a5450c3532092b91f81d274360e613c2371bc874a89c7a1a3c5e8d138/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7571e4464cb6e434958f867f7f730b8ab0b75e3f8e5eac0499168486ab3c33a8", size = 465650, upload-time = "2026-05-18T04:30:12.701Z" },
+    { url = "https://files.pythonhosted.org/packages/22/24/ed72f68cbc1333ca9b9f2200aa048bb6658ae41709bc1caad4310f4bdffd/watchfiles-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e53a384f76b631c3ae5334ce6a52f0baa3a911eb94a4eac7f160079868b716d5", size = 456398, upload-time = "2026-05-18T04:30:13.784Z" },
+    { url = "https://files.pythonhosted.org/packages/0d/64/982ef4a4e5bab5b6e5b6becc8cd5e732f6130a78b855f0abec6439a9a135/watchfiles-1.2.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:d20029a60a71a052a24c4db7673bc4de39ab89adbaccbfb5d67987c5d73f424d", size = 465140, upload-time = "2026-05-18T04:31:52.111Z" },
+    { url = "https://files.pythonhosted.org/packages/a0/0c/95282abf4ed680b6096010bcfc30c5fa7a041fc5aa5a2ad17a2cc6c75bba/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:2cb93af48550faf1cea04c303107c8b75833de7013e57ce27d3b8d21d8d0f58c", size = 630259, upload-time = "2026-05-18T04:31:25.676Z" },
+    { url = "https://files.pythonhosted.org/packages/30/45/607c1de1530c4bdcf2cf1d1ecc2505ddba5d96bd43ba9f2b0e79876f850f/watchfiles-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2995c176de7692b86a2e4c58d9ec718f753150a979cb4a754e2b4ffa38e70906", size = 659859, upload-time = "2026-05-18T04:30:24.333Z" },
+    { url = "https://files.pythonhosted.org/packages/fa/08/d9e2e0f9e8e6791d33aefc694ad7eefa7f901f63caff84a81ded38692f9c/watchfiles-1.2.0-cp312-cp312-win32.whl", hash = "sha256:7a2cffd17d27d2ecbb310c2b1d8174f222a5495b1a721894afa88ec11e25b898", size = 275480, upload-time = "2026-05-18T04:30:31.307Z" },
+    { url = "https://files.pythonhosted.org/packages/1c/e6/9d42569c0102645cc8cea5d8c7d8a1e9d4ada2cb7f05f75e554b8aa2202a/watchfiles-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:f155b3a1b2a5fc89cdc70d47ee5d54e3b75e88efa34982028a35daef9ba00379", size = 288718, upload-time = "2026-05-18T04:32:10.745Z" },
+    { url = "https://files.pythonhosted.org/packages/0a/26/88e0dc6ee3898169d7fa22bb6a69cabf2502d2ee25cb8c876d1262d204f8/watchfiles-1.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:8fa585ede612ee9f9e91b18bebf9ba11b9ae29a4e3a0d0cf6fca3e382133f0d5", size = 281026, upload-time = "2026-05-18T04:30:22.23Z" },
+    { url = "https://files.pythonhosted.org/packages/d1/4d/70a7feced9f87e2ff26dba42667290f41694fc64646c67261fbb8cab5d5c/watchfiles-1.2.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:01ea8d66f0693b9b60a6541c8d10263091ca9a9060d242f3c1f3143f9aad2c98", size = 399730, upload-time = "2026-05-18T04:31:38.162Z" },
+    { url = "https://files.pythonhosted.org/packages/31/3a/0da302f2307aee316922806ebd5726c542cbd787c938271cf14a074c7daf/watchfiles-1.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ba0480b9a74af058f43b337e937a451e109295c420916d68ad24e3dc02f5e44", size = 392842, upload-time = "2026-05-18T04:30:27.051Z" },
+    { url = "https://files.pythonhosted.org/packages/db/ef/d5bdb705c224dbc256aa0c1ec47bf4e61ec52558f2afb44a71a1fe4d7015/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f34e26a19f91f710c08e0183429f0d1d15df734e6bc78c31e77b9ea9c433658", size = 452989, upload-time = "2026-05-18T04:31:11.945Z" },
+    { url = "https://files.pythonhosted.org/packages/71/29/5495f2c1661949ef7a35e4d71111d129cfe7606414a26887a919d0a55406/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b4e77f6a55f858504069abd35d336a637555c09bca453dde1ee1e5ada8a6a1fb", size = 458978, upload-time = "2026-05-18T04:30:52.606Z" },
+    { url = "https://files.pythonhosted.org/packages/d5/8c/7f9c07c433811c2fffd93e13fdfb7135de9aab5f2ae41be08960fa0047dc/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb4d80e212f116474a545c21c912b445f16bb0cef9e6a73a498164223e14e2f", size = 490248, upload-time = "2026-05-18T04:31:36.003Z" },
+    { url = "https://files.pythonhosted.org/packages/3c/11/d93632febc52fbc21be90231bb7c17fd5387f46c9076fd40a5f9c2ae6910/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b974946a10af379d425e2eef5b62f5c6ebeaccf91d45eaad6f5b27ecd4f91aa0", size = 571847, upload-time = "2026-05-18T04:31:10.862Z" },
+    { url = "https://files.pythonhosted.org/packages/55/b4/383173e73aabb07ad1d9c7aa859d95437ac46a6d6a1e11005facda0c9d19/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86bc13c25a8d1fcd70b51d0ce7c9b65e90de5666fcbfd3e34957cc73ee19aeb5", size = 465974, upload-time = "2026-05-18T04:30:17.006Z" },
+    { url = "https://files.pythonhosted.org/packages/a7/6c/89b1a230a78f57c52dd8893adb1f92f94411721b6ec12596c56d98c74356/watchfiles-1.2.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca148d73dea36c9763aaa351e4d7a51780ec1584217c45276f4fe8239c768b71", size = 454782, upload-time = "2026-05-18T04:30:35.656Z" },
+    { url = "https://files.pythonhosted.org/packages/24/62/1732118367cfff0a9fce3bf62ff4bfded09ef5df21d9d446b858b3f70a96/watchfiles-1.2.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:c525543d91961c6955b2636b308569e84a1d1c5f5f2932041ab9ef46422f43e3", size = 465182, upload-time = "2026-05-18T04:30:20.846Z" },
+    { url = "https://files.pythonhosted.org/packages/28/96/716f7e5f51339bf22963f3345f9f27d7f3b30e2eadc597e257c881dd3c53/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:a204794696ffb8f9b10fba6f7cb5216d42f3b2b71860ccac6b6e42f5f10973b0", size = 629841, upload-time = "2026-05-18T04:31:05.397Z" },
+    { url = "https://files.pythonhosted.org/packages/4c/fe/c40783950fd771ccf66ab3ec2722d188a9af1c7f96c6e811f36e40c6e03f/watchfiles-1.2.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:10d86db20695afe7997ac9e1717637d6714a8d0220458c33f3d2061f54cec427", size = 658028, upload-time = "2026-05-18T04:31:48.22Z" },
+    { url = "https://files.pythonhosted.org/packages/71/72/4508db1856d1d87fcbb3b63f4839bab1b5682cb0e8d224d122263c09654a/watchfiles-1.2.0-cp313-cp313-win32.whl", hash = "sha256:eb283ee99e21ad6443c8cdb06ac5b34b1308c329cbdf03fa02b445363714c799", size = 275183, upload-time = "2026-05-18T04:30:59.57Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/36/14b76ca57652e5cc5fd1c11f32a261292c08a0d19a00351013c2549cbfb2/watchfiles-1.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:a0f27f01bee51861392bb6b7c4fdb290b27d1eb194e9e28788d68102a0e898d9", size = 288059, upload-time = "2026-05-18T04:32:07.937Z" },
+    { url = "https://files.pythonhosted.org/packages/1b/8d/0a85e395398d8d20fadfe5c5d32c726eee17a519e78fb356f2cf7531bffe/watchfiles-1.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:3651aa7058595e9cfb75d35dd5ada2bf9f48a5b8a0f3562821d3e210c507e077", size = 280186, upload-time = "2026-05-18T04:31:54.484Z" },
+    { url = "https://files.pythonhosted.org/packages/37/68/36db056f1fdcc5f07302f56e631774d6835bcd6fa3ace402304621d5f9e5/watchfiles-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:faea288b6f0ab1902ef08f4ca6de005dccf856c4e0c4f21b8c5fce02d90a1b08", size = 399031, upload-time = "2026-05-18T04:30:44.576Z" },
+    { url = "https://files.pythonhosted.org/packages/c1/64/01a9d6f66a82a5c101ce939274106cc72759d62427e153f01edd2b9f87c2/watchfiles-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01859b11fd9fbca670f4d5da00fbac282cfea9bd67a2125d8b2833a3b5617ea9", size = 391205, upload-time = "2026-05-18T04:30:25.413Z" },
+    { url = "https://files.pythonhosted.org/packages/84/2c/0a44fe058cb4bb7b8ede6b6670698bbb7c0400740e378d00022189b7b31d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fff610d7bb2256a317bb1e96f0d7862c7aa8076733ee5df0fd41bbe76a24a4f4", size = 451892, upload-time = "2026-05-18T04:32:14.005Z" },
+    { url = "https://files.pythonhosted.org/packages/67/a1/351e0d56cd35e6488b5c8b4fb11a809a5bc923e8fe8fed9faf8920be0c89/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b141a4891c995a039cd89e9a49e62df1dc8a559a5d1a6e4c7106d16c12777a55", size = 458867, upload-time = "2026-05-18T04:31:22.279Z" },
+    { url = "https://files.pythonhosted.org/packages/d5/7d/9d09605187f1b838998624049fcf8bf47b73c1a3b76901fcac1782f62277/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f22943b7770483f6ea0721c6b11d022947a98eb0acae14694de034f4d0d38925", size = 490217, upload-time = "2026-05-18T04:31:43.657Z" },
+    { url = "https://files.pythonhosted.org/packages/60/5d/a17a16eccb182f04188cd308ec24b1a71a9b5c4e7098269cf35d9fa56d02/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bc6195825b7dcd217968bb1f801a60fd4c16e8eeab5bedc7fe917d7d5995ab4", size = 571458, upload-time = "2026-05-18T04:32:11.875Z" },
+    { url = "https://files.pythonhosted.org/packages/d3/3d/4dd457062083ab1938e5dfd45032eb425cee2ac817287ca8ff4356183e5d/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d4a4b147f5dca2a5d325a06a832fb43f345751adfbc63204aec30e0d9ca965a2", size = 464707, upload-time = "2026-05-18T04:30:43.492Z" },
+    { url = "https://files.pythonhosted.org/packages/c6/71/ea8c57b128f5383de74d0c7d2d9c57ad7c9a65a930c451bd25d524b295b7/watchfiles-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4543579a9bdb0c9560039b4ffddbdb39545707659fbc430ce4c10f3f68d557f9", size = 454663, upload-time = "2026-05-18T04:30:16.061Z" },
+    { url = "https://files.pythonhosted.org/packages/53/fd/2e812bf938406d7db351f0703ddd3fc6c061cf30d96153a77bc79a943a44/watchfiles-1.2.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:20aa0e708b920bde876a4aa82dc7dd6ebea228a63a67cda6632c2fc87b787efa", size = 463537, upload-time = "2026-05-18T04:31:44.9Z" },
+    { url = "https://files.pythonhosted.org/packages/86/56/d17a7f1dd1bc3035f1072694a551301272f1739c2d8e319c927cb9e29b38/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:d413349d565dab74297f2a63e84a097936be69bf8f3b3801f27f380e32040f44", size = 629194, upload-time = "2026-05-18T04:31:14.141Z" },
+    { url = "https://files.pythonhosted.org/packages/be/06/f1ff66bf5cae50aa4062779a0ecd0bbaf15e466195719074078947d9a17d/watchfiles-1.2.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:f28b2725eb8cce327b9b3ab02415c853011dc55c95832fe90de6bc56f5315f72", size = 656194, upload-time = "2026-05-18T04:31:47.14Z" },
+    { url = "https://files.pythonhosted.org/packages/e7/54/a9c7ea9a82a4ac65e7004c0a03920b5cdd2f9c3b678757d9cd425aa51d53/watchfiles-1.2.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8c8358484d5fa12ef34f05b7f4168eaf1932f408725ff6d023c33ec17bd79d4", size = 400205, upload-time = "2026-05-18T04:32:05.153Z" },
+    { url = "https://files.pythonhosted.org/packages/aa/5d/c9ab3534374a4a67450696905d6ef16a04405448b8dc52bd752ae50423d4/watchfiles-1.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f04b092229ad2c50126dd3c922c8822e51e605993764a33058d4a791ab42281", size = 392508, upload-time = "2026-05-18T04:30:54.849Z" },
+    { url = "https://files.pythonhosted.org/packages/26/ca/1ad30103535cf0cecd7b993e8d50edc5351b1820e38f2d22e3df58962feb/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a7ce236284f002a156f70add88efe5c70879cccbb658be0822c54b1306fc09d", size = 452448, upload-time = "2026-05-18T04:30:53.727Z" },
+    { url = "https://files.pythonhosted.org/packages/37/a1/ceee2cdf2afbd715fa07758d39c9859513eae411b23196f7fd039e5feedd/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b9909cc2b48468b575eefa944919e1fe8a36c5849d5c7c168f80a8c1db69398e", size = 459605, upload-time = "2026-05-18T04:30:23.312Z" },
+    { url = "https://files.pythonhosted.org/packages/e8/f6/421e30fd1cb3907a84ed92ab3f1983e37ba2dca015e9a894a048418417a2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a37faaed405c67e28e6be45a1fa4f206ef5a2860f27c237db9fa30704c38242", size = 490757, upload-time = "2026-05-18T04:30:47.358Z" },
+    { url = "https://files.pythonhosted.org/packages/41/b0/55ed1b97ed08be7bba6f9a541cac15f2a858e1d74d2b07b6da70a82aab00/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9649193aa27bd9ff2e80ff29bfaa93085496c7a3a377592823cc58b77ee88add", size = 568672, upload-time = "2026-05-18T04:30:38.915Z" },
+    { url = "https://files.pythonhosted.org/packages/d1/cf/d8ae8a80dd7bafab395ea7681c10237311bbf34d37704a8c744e7cf31fc7/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4e4ff8e37f99cf1da89e255e07c9c4b37c214038c4283707bdec308cb1b0ea1f", size = 464197, upload-time = "2026-05-18T04:30:09.914Z" },
+    { url = "https://files.pythonhosted.org/packages/7c/8a/3076c496ca8dafe0e8cd03fcebdfc47be4b1174b4e5b24ff6e396e6b3af2/watchfiles-1.2.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:054dc20fd2e3132b4c3883b4a00d72fd6e1f56fdaf89fccd12e8057d74cd74d7", size = 453181, upload-time = "2026-05-18T04:30:14.829Z" },
+    { url = "https://files.pythonhosted.org/packages/e5/10/9745e17c98e7b8a86454df0a3c7b5686bd650383f1e9f26e4ebcbd6cc0c0/watchfiles-1.2.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e140ed30ebde76796b686e67c182cff10ea2fbab186fafd1560f74bb5a473a6e", size = 465109, upload-time = "2026-05-18T04:30:28.123Z" },
+    { url = "https://files.pythonhosted.org/packages/8f/95/8ef4a95481d3e0cb52d62a06fa6e972e81424be2d9698b91a2fecca9904c/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:bb7e52ecf68ba46d22df23467b87cffeb2146908aa523ebfe803019618cfda06", size = 630653, upload-time = "2026-05-18T04:31:49.304Z" },
+    { url = "https://files.pythonhosted.org/packages/fd/e4/3b3bf36b0f829b50c6ebcb8d031583863c59f923d6a6af3d485e470d0fac/watchfiles-1.2.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:23282a321c8baf9b3a3c4afff673f9fe65eb7fdc2338d765ccad9d3d1916a5ba", size = 657838, upload-time = "2026-05-18T04:31:06.497Z" },
+    { url = "https://files.pythonhosted.org/packages/21/b1/6cbbb50c1f3002ab568777d44aa21206dfb8807a840990c4037523b51812/watchfiles-1.2.0-cp314-cp314-win32.whl", hash = "sha256:c0db965c5f79aa49fe672d297cf1febc5ad149b658594944f49a54a2b96270a7", size = 275108, upload-time = "2026-05-18T04:30:06.891Z" },
+    { url = "https://files.pythonhosted.org/packages/92/45/190ce6db8dcb4536682cf75d3889ff1a27182a58cb519d343cb6d9ea63d8/watchfiles-1.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:71283b39fd17e5408eb123bd37aeecfd9d54c81fc184421943208aadb879d103", size = 288441, upload-time = "2026-05-18T04:32:12.901Z" },
+    { url = "https://files.pythonhosted.org/packages/74/0d/3eae1c2313ab08378431d907c3f8095ecca00f3eda33111cf4f0f2591799/watchfiles-1.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:c5c19526f4e54a00f2666a6c0e9e40d582c09e865055ea7378bf0009aab857b3", size = 280684, upload-time = "2026-05-18T04:31:26.902Z" },
+    { url = "https://files.pythonhosted.org/packages/b1/75/fb64e6c25d6b5ca636d03df34ffb1c6e9873303e76d27967e045f8df088f/watchfiles-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:d73a585accffa5ae39c17264c36ec3166d2fad7000c780f5ef83b2722afb9dd2", size = 398857, upload-time = "2026-05-18T04:32:17.108Z" },
+    { url = "https://files.pythonhosted.org/packages/73/4e/9f7adf01754cbf81843722ccfec169d8f26c69778281a302855cecd2ee08/watchfiles-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae99b14c5f21e026e0e9d96f40e07d8570ebee6cafd9d8fc318354606daa7a28", size = 392413, upload-time = "2026-05-18T04:31:07.911Z" },
+    { url = "https://files.pythonhosted.org/packages/47/c8/bec626bcc2d69f44b9acb24ce7d60ed7b16b73628eea747fcbd169d8edda/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4429f3b105524a10b72c3a819b091c495d2811d419c1e1e8df773a5a5974f831", size = 452409, upload-time = "2026-05-18T04:31:20.142Z" },
+    { url = "https://files.pythonhosted.org/packages/00/b7/b6362068e81e7c556d155a34c35d40ac3ef42d747b06d7f6e5bf58e359c2/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:43d818978d06062d9b22c4fab2ebe44cf5213d42dc8e62bda8c2760cfa2eeb33", size = 458827, upload-time = "2026-05-18T04:32:06.219Z" },
+    { url = "https://files.pythonhosted.org/packages/67/f8/9a813fa42afb1e0b4625e75f0479826644d3ee8dc287e093799bc01f390c/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b9f732dc58b2dbe69e464ccf8fff7a03b0dd0be439da4c0720d3558527d3d6b4", size = 490104, upload-time = "2026-05-18T04:31:56.034Z" },
+    { url = "https://files.pythonhosted.org/packages/2f/bf/27dfb6094ca4c9aad21298b5525b6c53cb36121ee454331d05161e58d130/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f200104103feb097de4cab8fe4f5dd18a2026934c7dea98c55a2f5fd6d5a33b", size = 571360, upload-time = "2026-05-18T04:31:57.133Z" },
+    { url = "https://files.pythonhosted.org/packages/fb/39/44a096d67270ea93df91d33877dbe91fbda3aa4f8ec2edf799d93eda8736/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ac26eefbf4af1741247d6fb68b11c49a25b2f7413fbd318a83a12aaa9cf666", size = 464644, upload-time = "2026-05-18T04:30:57.33Z" },
+    { url = "https://files.pythonhosted.org/packages/0e/80/c7472203bad6268e3ef1ad260739704847898938ad7ea8b63a5131f46b50/watchfiles-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c4997d4e4a55f0d02b6cde327322daf3a0400e5df6c6b15948994bf72497925", size = 454771, upload-time = "2026-05-18T04:30:48.736Z" },
+    { url = "https://files.pythonhosted.org/packages/51/cf/3b10b268b4b7f0fc26e9debb5eef1998b515887840f444cd3ec80c688755/watchfiles-1.2.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:4c887eba18b7945ac73067a8b4a66f21cd46c2539b2bc68588f7be6c7eb6d26b", size = 463494, upload-time = "2026-05-18T04:31:33.826Z" },
+    { url = "https://files.pythonhosted.org/packages/3d/3e/a4302545cd589262a0dc7d140e86f7688eba3f9c72776c27f7e23b8864c4/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:3416ff151bb6b5a8d8d11664974fbef4d9305b9b2957839ab5a270468fd8df30", size = 629383, upload-time = "2026-05-18T04:31:15.596Z" },
+    { url = "https://files.pythonhosted.org/packages/db/99/d5649df0a9a410d45b7c882304d0b790903ac9b6e8f2cfd12114e0c6b9f2/watchfiles-1.2.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:0e831a271c035d89789cffc386b6aa1375f39f1cd25eb7ca0997e4970d152fc5", size = 656093, upload-time = "2026-05-18T04:31:58.707Z" },
+    { url = "https://files.pythonhosted.org/packages/92/b9/362702539275019a54dd2e94511b31a9b89c5f9e6a21966de7eb692549fc/watchfiles-1.2.0-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:37a6721cdf3f65dbb13aa9503510ccb4451603ac837e44d265d7992a597e1374", size = 400109, upload-time = "2026-05-18T04:31:16.879Z" },
+    { url = "https://files.pythonhosted.org/packages/8f/75/71d5ba62db781e5587bded1d944c675374bc4aa37ff33d5018d98e8b6538/watchfiles-1.2.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2b37d10b5a63bd4d87e18472d80fa525bd670586fae62e5dd580452764879b65", size = 392167, upload-time = "2026-05-18T04:31:28.058Z" },
+    { url = "https://files.pythonhosted.org/packages/3c/01/c66dd95d0423fe30d31820e2d1d5bda773764131bbb6ac0cb1cf303ac328/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a105bc2283f67e8fbec74253ec2d94925de92ed72c0393f1206bf326b7b7b69", size = 452372, upload-time = "2026-05-18T04:31:00.836Z" },
+    { url = "https://files.pythonhosted.org/packages/91/15/2fe99557e72f85627c6a8eed50d889e8d101623e060a22ad75b875cb932d/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5327989a465505f05cfe06f04fa9d0c2fd5432bb243e10e6f012b1bdca3c8579", size = 459596, upload-time = "2026-05-18T04:31:34.96Z" },
+    { url = "https://files.pythonhosted.org/packages/ed/23/d4acfa0023367428ed48351b3b9b267893037b6cadae55620c61c24bcfd4/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ecb47f183a8025b2aa18b546725c3657e542112ae9c0613a2af79b4fa8d04ad7", size = 490869, upload-time = "2026-05-18T04:31:59.923Z" },
+    { url = "https://files.pythonhosted.org/packages/a4/5f/3164cbdce06c9fb95c4f7b9e2f9760b5e2797af43a9ecc317ef42a23a278/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8520a4ab0e37f770afc34459c4f8f7019e153f9124dc101c15538365875d1ab2", size = 571641, upload-time = "2026-05-18T04:32:00.948Z" },
+    { url = "https://files.pythonhosted.org/packages/41/e6/85d3731c55e65cd7690f3f803d24c139588aaf863e4bf2148fe7a7fa1a19/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:71cd71740ed2c15211ebb237ced4e39a1cdf6f80566e5fe95428da1626f4fde6", size = 464444, upload-time = "2026-05-18T04:30:34.298Z" },
+    { url = "https://files.pythonhosted.org/packages/f4/7d/562641012b8b09872742c3b8adf9629ec479fd78f8d68ae4a0c13da8add6/watchfiles-1.2.0-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f88af53d6ddaf72179ef613ddc905e6f4785f712b49b80b3bef9f3525e6194b4", size = 453593, upload-time = "2026-05-18T04:31:23.464Z" },
+    { url = "https://files.pythonhosted.org/packages/56/fe/cb8ef3d6f929d14158fdaaad9925985b7310abc9384dcd4d82dd0016fb59/watchfiles-1.2.0-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:cee9d5efd929efdac5f7e58f72b3376f676b64050a91c5b99a7094c5b2317488", size = 465096, upload-time = "2026-05-18T04:31:30.384Z" },
+    { url = "https://files.pythonhosted.org/packages/25/91/80908e835e100527a9267147b08c0eee1fa6ab0ffec15edc04d1d44885f7/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_aarch64.whl", hash = "sha256:b718bf356bbc15e559bd8ef41782b573b8ae0e3f177ab244b440568d7ea02cfb", size = 630638, upload-time = "2026-05-18T04:30:49.89Z" },
+    { url = "https://files.pythonhosted.org/packages/46/4b/95ab2f256bb4af3cb2eb23b9317bda984ee6e0f11733a5c004a6c95b06e3/watchfiles-1.2.0-cp315-cp315-musllinux_1_1_x86_64.whl", hash = "sha256:922c0e019fe68b3ae392965a766b02a71ba1168c932cebc3733cd52c5fe5b377", size = 657684, upload-time = "2026-05-18T04:31:32.027Z" },
+    { url = "https://files.pythonhosted.org/packages/23/f4/7513ef1e85fc4c6331b59479d6d72661fc391fbe543678052ac72c8b6c19/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:4674d49eb94706dfe666c069fc0a1b646ffcf920473492e209f6d5f60d3f0cc2", size = 403050, upload-time = "2026-05-18T04:30:36.753Z" },
+    { url = "https://files.pythonhosted.org/packages/27/0b/a54103cfd732bb703c7a749222011a0483ef3705948dae3b203158601119/watchfiles-1.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:094b9b70103d4e963499bdea001ee3c2697b144cd9ae6218a62c0f89ec9e31db", size = 396629, upload-time = "2026-05-18T04:32:03.268Z" },
+    { url = "https://files.pythonhosted.org/packages/5e/2c/73f31a3b893886206c3f54d73e8ad8dee58cdb2f69ad2622e0a8a9e07f4e/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0ef001f8c25ad0fa9529f914c1600647ecd0f542d11c19b7894768c67b6acb7", size = 457318, upload-time = "2026-05-18T04:31:01.932Z" },
+    { url = "https://files.pythonhosted.org/packages/e9/f9/45d021e4a5cc7b9dd567f7cbb06d3b75f751a690063fb6cc7ec60f4e46b7/watchfiles-1.2.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a88fc94e647bc4eec523f1caa540258eb71d14278b9daf72fa1e2658a98df0f0", size = 457771, upload-time = "2026-05-18T04:30:56.331Z" },
+]
+
+[[package]]
+name = "websockets"
+version = "16.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/04/24/4b2031d72e840ce4c1ccb255f693b15c334757fc50023e4db9537080b8c4/websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5", size = 179346, upload-time = "2026-01-10T09:23:47.181Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" },
+    { url = "https://files.pythonhosted.org/packages/f3/fa/abe89019d8d8815c8781e90d697dec52523fb8ebe308bf11664e8de1877e/websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad", size = 175022, upload-time = "2026-01-10T09:22:36.332Z" },
+    { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" },
+    { url = "https://files.pythonhosted.org/packages/d2/ae/0ee92b33087a33632f37a635e11e1d99d429d3d323329675a6022312aac2/websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe", size = 184631, upload-time = "2026-01-10T09:22:38.789Z" },
+    { url = "https://files.pythonhosted.org/packages/c8/c5/27178df583b6c5b31b29f526ba2da5e2f864ecc79c99dae630a85d68c304/websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b", size = 185870, upload-time = "2026-01-10T09:22:39.893Z" },
+    { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" },
+    { url = "https://files.pythonhosted.org/packages/6d/e2/d5332c90da12b1e01f06fb1b85c50cfc489783076547415bf9f0a659ec19/websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64", size = 184615, upload-time = "2026-01-10T09:22:42.442Z" },
+    { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" },
+    { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" },
+    { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" },
+    { url = "https://files.pythonhosted.org/packages/b0/fe/136ccece61bd690d9c1f715baaeefd953bb2360134de73519d5df19d29ca/websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79", size = 175038, upload-time = "2026-01-10T09:22:47.999Z" },
+    { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" },
+    { url = "https://files.pythonhosted.org/packages/18/29/71729b4671f21e1eaa5d6573031ab810ad2936c8175f03f97f3ff164c802/websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c", size = 184915, upload-time = "2026-01-10T09:22:51.071Z" },
+    { url = "https://files.pythonhosted.org/packages/97/bb/21c36b7dbbafc85d2d480cd65df02a1dc93bf76d97147605a8e27ff9409d/websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f", size = 186152, upload-time = "2026-01-10T09:22:52.224Z" },
+    { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" },
+    { url = "https://files.pythonhosted.org/packages/47/88/4dd516068e1a3d6ab3c7c183288404cd424a9a02d585efbac226cb61ff2d/websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2", size = 184880, upload-time = "2026-01-10T09:22:55.033Z" },
+    { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" },
+    { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" },
+    { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" },
+    { url = "https://files.pythonhosted.org/packages/7e/0c/8811fc53e9bcff68fe7de2bcbe75116a8d959ac699a3200f4847a8925210/websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230", size = 175039, upload-time = "2026-01-10T09:23:01.171Z" },
+    { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" },
+    { url = "https://files.pythonhosted.org/packages/bd/28/0a25ee5342eb5d5f297d992a77e56892ecb65e7854c7898fb7d35e9b33bd/websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5", size = 184975, upload-time = "2026-01-10T09:23:03.756Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/66/27ea52741752f5107c2e41fda05e8395a682a1e11c4e592a809a90c6a506/websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82", size = 186203, upload-time = "2026-01-10T09:23:05.01Z" },
+    { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" },
+    { url = "https://files.pythonhosted.org/packages/9b/67/f926bac29882894669368dc73f4da900fcdf47955d0a0185d60103df5737/websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f", size = 184920, upload-time = "2026-01-10T09:23:07.492Z" },
+    { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" },
+    { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" },
+    { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" },
+    { url = "https://files.pythonhosted.org/packages/f2/78/e63be1bf0724eeb4616efb1ae1c9044f7c3953b7957799abb5915bffd38e/websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904", size = 175085, upload-time = "2026-01-10T09:23:13.511Z" },
+    { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" },
+    { url = "https://files.pythonhosted.org/packages/63/bc/d3e208028de777087e6fb2b122051a6ff7bbcca0d6df9d9c2bf1dd869ae9/websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e", size = 185044, upload-time = "2026-01-10T09:23:15.939Z" },
+    { url = "https://files.pythonhosted.org/packages/ad/6e/9a0927ac24bd33a0a9af834d89e0abc7cfd8e13bed17a86407a66773cc0e/websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4", size = 186279, upload-time = "2026-01-10T09:23:17.148Z" },
+    { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" },
+    { url = "https://files.pythonhosted.org/packages/c4/f8/fdc34643a989561f217bb477cbc47a3a07212cbda91c0e4389c43c296ebf/websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3", size = 184982, upload-time = "2026-01-10T09:23:19.652Z" },
+    { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" },
+    { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" },
+    { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" },
+    { url = "https://files.pythonhosted.org/packages/bd/40/53ad02341fa33b3ce489023f635367a4ac98b73570102ad2cdd770dacc9a/websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e", size = 175268, upload-time = "2026-01-10T09:23:25.781Z" },
+    { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" },
+    { url = "https://files.pythonhosted.org/packages/e5/2d/7583b30208b639c8090206f95073646c2c9ffd66f44df967981a64f849ad/websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8", size = 185331, upload-time = "2026-01-10T09:23:28.259Z" },
+    { url = "https://files.pythonhosted.org/packages/45/b0/cce3784eb519b7b5ad680d14b9673a31ab8dcb7aad8b64d81709d2430aa8/websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e", size = 186501, upload-time = "2026-01-10T09:23:29.449Z" },
+    { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" },
+    { url = "https://files.pythonhosted.org/packages/88/a8/a080593f89b0138b6cba1b28f8df5673b5506f72879322288b031337c0b8/websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206", size = 185356, upload-time = "2026-01-10T09:23:32.627Z" },
+    { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" },
+    { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" },
+    { url = "https://files.pythonhosted.org/packages/72/07/c98a68571dcf256e74f1f816b8cc5eae6eb2d3d5cfa44d37f801619d9166/websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d", size = 174947, upload-time = "2026-01-10T09:23:36.166Z" },
+    { url = "https://files.pythonhosted.org/packages/7e/52/93e166a81e0305b33fe416338be92ae863563fe7bce446b0f687b9df5aea/websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03", size = 175260, upload-time = "2026-01-10T09:23:37.409Z" },
+    { url = "https://files.pythonhosted.org/packages/56/0c/2dbf513bafd24889d33de2ff0368190a0e69f37bcfa19009ef819fe4d507/websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da", size = 176071, upload-time = "2026-01-10T09:23:39.158Z" },
+    { url = "https://files.pythonhosted.org/packages/a5/8f/aea9c71cc92bf9b6cc0f7f70df8f0b420636b6c96ef4feee1e16f80f75dd/websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c", size = 176968, upload-time = "2026-01-10T09:23:41.031Z" },
+    { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" },
+    { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" },
+]

+ 4 - 0
go/.gitignore

@@ -0,0 +1,4 @@
+mnemonicode
+mnemonicDECODE
+__pycache__/
+*.pyc

+ 3 - 0
go/go.mod

@@ -0,0 +1,3 @@
+module mnemonicode
+
+go 1.26.4

+ 162 - 0
go/mnemonicode.go

@@ -0,0 +1,162 @@
+// Mnemonicode encoder/decoder.
+// Encodes arbitrary bytes as human-readable words.
+// Each group of 4 bytes maps to 3 words; trailing groups use fewer words.
+
+package main
+
+import (
+  "embed"
+  "encoding/json"
+  "flag"
+  "fmt"
+  "io"
+  "os"
+  "path/filepath"
+  "strings"
+)
+
+//go:embed wordlist.json
+var wordlistJSON []byte
+
+const mnBase = 1626
+
+var (
+  words     []string
+  wordIndex map[string]int
+)
+
+func init() {
+  var data struct {
+    Words []string `json:"words"`
+  }
+  if err := json.Unmarshal(wordlistJSON, &data); err != nil {
+    panic("failed to parse embedded wordlist: " + err.Error())
+  }
+  words = data.Words
+  wordIndex = make(map[string]int, len(words))
+  for i, w := range words {
+    wordIndex[w] = i
+  }
+}
+
+func wordsRequired(size int) int {
+  return ((size + 1) * 3) / 4
+}
+
+func encode(data []byte) []string {
+  result := make([]string, 0, wordsRequired(len(data)))
+  for n := 0; n < wordsRequired(len(data)); n++ {
+    grp := (n / 3) * 4
+    remaining := len(data) - grp
+    if remaining > 4 {
+      remaining = 4
+    }
+    chunk := make([]byte, 4)
+    copy(chunk, data[grp:grp+remaining])
+    x := uint32(chunk[0]) | uint32(chunk[1])<<8 | uint32(chunk[2])<<16 | uint32(chunk[3])<<24
+    pos := n % 3
+    extra := 0
+    if pos == 2 {
+      if remaining == 3 {
+        extra = mnBase
+      }
+      x /= mnBase * mnBase
+    } else if pos == 1 {
+      x /= mnBase
+    }
+    result = append(result, words[x%mnBase+uint32(extra)])
+  }
+  return result
+}
+
+func decode(encoded []string) ([]byte, error) {
+  groups := len(encoded)/3 + 1
+  buf := make([]byte, groups*4)
+  offset := 0
+
+  getU32 := func(pos int) uint32 {
+    grp := pos & ^3
+    return uint32(buf[grp]) | uint32(buf[grp+1])<<8 | uint32(buf[grp+2])<<16 | uint32(buf[grp+3])<<24
+  }
+  setU32 := func(pos int, v uint32) {
+    grp := pos & ^3
+    buf[grp] = byte(v)
+    buf[grp+1] = byte(v >> 8)
+    buf[grp+2] = byte(v >> 16)
+    buf[grp+3] = byte(v >> 24)
+  }
+
+  for _, word := range encoded {
+    idx, ok := wordIndex[strings.ToLower(word)]
+    if !ok {
+      return nil, fmt.Errorf("unrecognized word: %q", word)
+    }
+
+    pos := offset % 4
+    x := getU32(offset)
+
+    switch pos {
+    case 3:
+      return nil, fmt.Errorf("unexpected word after 24-bit remainder")
+    case 0:
+      x = uint32(idx)
+      offset++
+    case 1:
+      x += uint32(idx) * mnBase
+      offset++
+    case 2:
+      if idx >= mnBase {
+        x += uint32(idx-mnBase) * mnBase * mnBase
+        offset++ // offset%4 == 3 signals end-of-group
+      } else {
+        if idx > 1624 || (idx == 1624 && x > 1_312_671) {
+          return nil, fmt.Errorf("invalid encoding: value out of range")
+        }
+        x += uint32(idx) * mnBase * mnBase
+        offset += 2
+      }
+    }
+    setU32(offset-1, x)
+  }
+
+  return buf[:offset], nil
+}
+
+func main() {
+  sep := flag.String("s", "-", "word separator")
+  flag.StringVar(sep, "separator", "-", "word separator")
+  flag.Parse()
+
+  decoding := strings.Contains(strings.ToLower(filepath.Base(os.Args[0])), "decode")
+
+  if decoding {
+    raw, err := io.ReadAll(os.Stdin)
+    if err != nil {
+      fmt.Fprintf(os.Stderr, "error reading stdin: %v\n", err)
+      os.Exit(1)
+    }
+    parts := strings.FieldsFunc(string(raw), func(r rune) bool {
+      return strings.ContainsRune(*sep, r) || r == '\n' || r == '\r'
+    })
+    var filtered []string
+    for _, p := range parts {
+      p = strings.TrimSpace(p)
+      if p != "" {
+        filtered = append(filtered, p)
+      }
+    }
+    out, err := decode(filtered)
+    if err != nil {
+      fmt.Fprintf(os.Stderr, "error: %v\n", err)
+      os.Exit(1)
+    }
+    os.Stdout.Write(out)
+  } else {
+    data, err := io.ReadAll(os.Stdin)
+    if err != nil {
+      fmt.Fprintf(os.Stderr, "error reading stdin: %v\n", err)
+      os.Exit(1)
+    }
+    fmt.Println(strings.Join(encode(data), *sep))
+  }
+}

+ 124 - 0
go/mnemonicode.py

@@ -0,0 +1,124 @@
+"""
+Pure-Python implementation of mnemonicode encoding/decoding.
+
+Mnemonicode encodes arbitrary bytes as a sequence of human-readable words.
+Each group of 4 bytes maps to 3 words; shorter trailing groups use fewer words.
+The 24-bit trailing case (3 bytes) uses a distinct set of 7 short words to
+signal that a remainder word ends the group.
+
+Word list: version 0.7, 1626 base words + 7 remainder words = 1633 total.
+"""
+
+import json
+import sys
+from functools import lru_cache
+from pathlib import Path
+
+MN_BASE = 1626
+
+_WORDLIST_JSON = Path(__file__).parent / "wordlist.json"
+
+
+@lru_cache(maxsize=1)
+def _load() -> tuple[list[str], dict[str, int], str]:
+  data = json.loads(_WORDLIST_JSON.read_text())
+  words: list[str] = data["words"]
+  index: dict[str, int] = {w: i for i, w in enumerate(words)}
+  return words, index, data["version"]
+
+
+def wordlist_version() -> str:
+  return _load()[2]
+
+
+def words_required(size: int) -> int:
+  """Number of mnemonic words needed to encode `size` bytes."""
+  return ((size + 1) * 3) // 4
+
+
+def encode(data: bytes) -> list[str]:
+  """Encode bytes to a list of mnemonic words."""
+  words, _, _ = _load()
+  result = []
+  for n in range(words_required(len(data))):
+    grp = (n // 3) * 4
+    remaining = min(len(data) - grp, 4)
+    x = int.from_bytes(data[grp:grp + remaining], "little")
+    pos = n % 3
+    extra = 0
+    if pos == 2:
+      if remaining == 3:
+        extra = MN_BASE  # use one of the 7 short remainder words
+      x //= MN_BASE * MN_BASE
+    elif pos == 1:
+      x //= MN_BASE
+    result.append(words[x % MN_BASE + extra])
+  return result
+
+
+def decode(encoded: list[str]) -> bytes:
+  """Decode a list of mnemonic words back to bytes."""
+  _, word_index, _ = _load()
+
+  buf = bytearray(((len(encoded) // 3) + 1) * 4)
+  offset = 0
+
+  for word in encoded:
+    try:
+      idx = word_index[word.lower()]
+    except KeyError:
+      raise ValueError(f"unrecognized word: {word!r}") from None
+
+    grp = offset & ~3
+    x = int.from_bytes(buf[grp:grp + 4], "little")
+    pos = offset % 4
+
+    if pos == 3:
+      raise ValueError("unexpected word after 24-bit remainder")
+    elif pos == 0:
+      x = idx
+      offset += 1
+    elif pos == 1:
+      x += idx * MN_BASE
+      offset += 1
+    elif pos == 2:
+      if idx >= MN_BASE:  # 24-bit remainder word
+        x += (idx - MN_BASE) * MN_BASE * MN_BASE
+        offset += 1  # offset % 4 == 3 signals end-of-group next
+      else:
+        if idx > 1624 or (idx == 1624 and x > 1_312_671):
+          raise ValueError("invalid encoding: value out of range")
+        x += idx * MN_BASE * MN_BASE
+        offset += 2  # skip to next group
+
+    buf[grp:grp + 4] = x.to_bytes(4, "little")
+
+  return bytes(buf[:offset])
+
+
+if __name__ == "__main__":
+  import argparse
+
+  parser = argparse.ArgumentParser(
+    prog="mnemonicode",
+    description="Encode bytes to mnemonic words or decode words back to bytes.",
+  )
+  parser.add_argument("-s", "--separator", default="-", help="word separator (default: -)")
+  sub = parser.add_subparsers(dest="cmd", required=True)
+  sub.add_parser("encode", help="read raw bytes from stdin, write words to stdout")
+  sub.add_parser("decode", help="read words from stdin, write raw bytes to stdout")
+
+  args = parser.parse_args()
+
+  if args.cmd == "encode":
+    data = sys.stdin.buffer.read()
+    print(args.separator.join(encode(data)))
+
+  elif args.cmd == "decode":
+    words = [w.strip() for w in sys.stdin.read().split(args.separator) if w.strip()]
+    try:
+      data = decode(words)
+    except ValueError as e:
+      print(f"error: {e}", file=sys.stderr)
+      sys.exit(1)
+    sys.stdout.buffer.write(data)

+ 1638 - 0
go/wordlist.json

@@ -0,0 +1,1638 @@
+{
+  "version": "0.7",
+  "words": [
+    "academy",
+    "acrobat",
+    "active",
+    "actor",
+    "adam",
+    "admiral",
+    "adrian",
+    "africa",
+    "agenda",
+    "agent",
+    "airline",
+    "airport",
+    "aladdin",
+    "alarm",
+    "alaska",
+    "albert",
+    "albino",
+    "album",
+    "alcohol",
+    "alex",
+    "algebra",
+    "alibi",
+    "alice",
+    "alien",
+    "alpha",
+    "alpine",
+    "amadeus",
+    "amanda",
+    "amazon",
+    "amber",
+    "america",
+    "amigo",
+    "analog",
+    "anatomy",
+    "angel",
+    "animal",
+    "antenna",
+    "antonio",
+    "apollo",
+    "april",
+    "archive",
+    "arctic",
+    "arizona",
+    "arnold",
+    "aroma",
+    "arthur",
+    "artist",
+    "asia",
+    "aspect",
+    "aspirin",
+    "athena",
+    "athlete",
+    "atlas",
+    "audio",
+    "august",
+    "austria",
+    "axiom",
+    "aztec",
+    "balance",
+    "ballad",
+    "banana",
+    "bandit",
+    "banjo",
+    "barcode",
+    "baron",
+    "basic",
+    "battery",
+    "belgium",
+    "berlin",
+    "bermuda",
+    "bernard",
+    "bikini",
+    "binary",
+    "bingo",
+    "biology",
+    "block",
+    "blonde",
+    "bonus",
+    "boris",
+    "boston",
+    "boxer",
+    "brandy",
+    "bravo",
+    "brazil",
+    "bronze",
+    "brown",
+    "bruce",
+    "bruno",
+    "burger",
+    "burma",
+    "cabinet",
+    "cactus",
+    "cafe",
+    "cairo",
+    "cake",
+    "calypso",
+    "camel",
+    "camera",
+    "campus",
+    "canada",
+    "canal",
+    "cannon",
+    "canoe",
+    "cantina",
+    "canvas",
+    "canyon",
+    "capital",
+    "caramel",
+    "caravan",
+    "carbon",
+    "cargo",
+    "carlo",
+    "carol",
+    "carpet",
+    "cartel",
+    "casino",
+    "castle",
+    "castro",
+    "catalog",
+    "caviar",
+    "cecilia",
+    "cement",
+    "center",
+    "century",
+    "ceramic",
+    "chamber",
+    "chance",
+    "change",
+    "chaos",
+    "charlie",
+    "charm",
+    "charter",
+    "chef",
+    "chemist",
+    "cherry",
+    "chess",
+    "chicago",
+    "chicken",
+    "chief",
+    "china",
+    "cigar",
+    "cinema",
+    "circus",
+    "citizen",
+    "city",
+    "clara",
+    "classic",
+    "claudia",
+    "clean",
+    "client",
+    "climax",
+    "clinic",
+    "clock",
+    "club",
+    "cobra",
+    "coconut",
+    "cola",
+    "collect",
+    "colombo",
+    "colony",
+    "color",
+    "combat",
+    "comedy",
+    "comet",
+    "command",
+    "compact",
+    "company",
+    "complex",
+    "concept",
+    "concert",
+    "connect",
+    "consul",
+    "contact",
+    "context",
+    "contour",
+    "control",
+    "convert",
+    "copy",
+    "corner",
+    "corona",
+    "correct",
+    "cosmos",
+    "couple",
+    "courage",
+    "cowboy",
+    "craft",
+    "crash",
+    "credit",
+    "cricket",
+    "critic",
+    "crown",
+    "crystal",
+    "cuba",
+    "culture",
+    "dallas",
+    "dance",
+    "daniel",
+    "david",
+    "decade",
+    "decimal",
+    "deliver",
+    "delta",
+    "deluxe",
+    "demand",
+    "demo",
+    "denmark",
+    "derby",
+    "design",
+    "detect",
+    "develop",
+    "diagram",
+    "dialog",
+    "diamond",
+    "diana",
+    "diego",
+    "diesel",
+    "diet",
+    "digital",
+    "dilemma",
+    "diploma",
+    "direct",
+    "disco",
+    "disney",
+    "distant",
+    "doctor",
+    "dollar",
+    "dominic",
+    "domino",
+    "donald",
+    "dragon",
+    "drama",
+    "dublin",
+    "duet",
+    "dynamic",
+    "east",
+    "ecology",
+    "economy",
+    "edgar",
+    "egypt",
+    "elastic",
+    "elegant",
+    "element",
+    "elite",
+    "elvis",
+    "email",
+    "energy",
+    "engine",
+    "english",
+    "episode",
+    "equator",
+    "escort",
+    "ethnic",
+    "europe",
+    "everest",
+    "evident",
+    "exact",
+    "example",
+    "exit",
+    "exotic",
+    "export",
+    "express",
+    "extra",
+    "fabric",
+    "factor",
+    "falcon",
+    "family",
+    "fantasy",
+    "fashion",
+    "fiber",
+    "fiction",
+    "fidel",
+    "fiesta",
+    "figure",
+    "film",
+    "filter",
+    "final",
+    "finance",
+    "finish",
+    "finland",
+    "flash",
+    "florida",
+    "flower",
+    "fluid",
+    "flute",
+    "focus",
+    "ford",
+    "forest",
+    "formal",
+    "format",
+    "formula",
+    "fortune",
+    "forum",
+    "fragile",
+    "france",
+    "frank",
+    "friend",
+    "frozen",
+    "future",
+    "gabriel",
+    "galaxy",
+    "gallery",
+    "gamma",
+    "garage",
+    "garden",
+    "garlic",
+    "gemini",
+    "general",
+    "genetic",
+    "genius",
+    "germany",
+    "global",
+    "gloria",
+    "golf",
+    "gondola",
+    "gong",
+    "good",
+    "gordon",
+    "gorilla",
+    "grand",
+    "granite",
+    "graph",
+    "green",
+    "group",
+    "guide",
+    "guitar",
+    "guru",
+    "hand",
+    "happy",
+    "harbor",
+    "harmony",
+    "harvard",
+    "havana",
+    "hawaii",
+    "helena",
+    "hello",
+    "henry",
+    "hilton",
+    "history",
+    "horizon",
+    "hotel",
+    "human",
+    "humor",
+    "icon",
+    "idea",
+    "igloo",
+    "igor",
+    "image",
+    "impact",
+    "import",
+    "index",
+    "india",
+    "indigo",
+    "input",
+    "insect",
+    "instant",
+    "iris",
+    "italian",
+    "jacket",
+    "jacob",
+    "jaguar",
+    "janet",
+    "japan",
+    "jargon",
+    "jazz",
+    "jeep",
+    "john",
+    "joker",
+    "jordan",
+    "jumbo",
+    "june",
+    "jungle",
+    "junior",
+    "jupiter",
+    "karate",
+    "karma",
+    "kayak",
+    "kermit",
+    "kilo",
+    "king",
+    "koala",
+    "korea",
+    "labor",
+    "lady",
+    "lagoon",
+    "laptop",
+    "laser",
+    "latin",
+    "lava",
+    "lecture",
+    "left",
+    "legal",
+    "lemon",
+    "level",
+    "lexicon",
+    "liberal",
+    "libra",
+    "limbo",
+    "limit",
+    "linda",
+    "linear",
+    "lion",
+    "liquid",
+    "liter",
+    "little",
+    "llama",
+    "lobby",
+    "lobster",
+    "local",
+    "logic",
+    "logo",
+    "lola",
+    "london",
+    "lotus",
+    "lucas",
+    "lunar",
+    "machine",
+    "macro",
+    "madam",
+    "madonna",
+    "madrid",
+    "maestro",
+    "magic",
+    "magnet",
+    "magnum",
+    "major",
+    "mama",
+    "mambo",
+    "manager",
+    "mango",
+    "manila",
+    "marco",
+    "marina",
+    "market",
+    "mars",
+    "martin",
+    "marvin",
+    "master",
+    "matrix",
+    "maximum",
+    "media",
+    "medical",
+    "mega",
+    "melody",
+    "melon",
+    "memo",
+    "mental",
+    "mentor",
+    "menu",
+    "mercury",
+    "message",
+    "metal",
+    "meteor",
+    "meter",
+    "method",
+    "metro",
+    "mexico",
+    "miami",
+    "micro",
+    "million",
+    "mineral",
+    "minimum",
+    "minus",
+    "minute",
+    "miracle",
+    "mirage",
+    "miranda",
+    "mister",
+    "mixer",
+    "mobile",
+    "model",
+    "modem",
+    "modern",
+    "modular",
+    "moment",
+    "monaco",
+    "monica",
+    "monitor",
+    "mono",
+    "monster",
+    "montana",
+    "morgan",
+    "motel",
+    "motif",
+    "motor",
+    "mozart",
+    "multi",
+    "museum",
+    "music",
+    "mustang",
+    "natural",
+    "neon",
+    "nepal",
+    "neptune",
+    "nerve",
+    "neutral",
+    "nevada",
+    "news",
+    "ninja",
+    "nirvana",
+    "normal",
+    "nova",
+    "novel",
+    "nuclear",
+    "numeric",
+    "nylon",
+    "oasis",
+    "object",
+    "observe",
+    "ocean",
+    "octopus",
+    "olivia",
+    "olympic",
+    "omega",
+    "opera",
+    "optic",
+    "optimal",
+    "orange",
+    "orbit",
+    "organic",
+    "orient",
+    "origin",
+    "orlando",
+    "oscar",
+    "oxford",
+    "oxygen",
+    "ozone",
+    "pablo",
+    "pacific",
+    "pagoda",
+    "palace",
+    "pamela",
+    "panama",
+    "panda",
+    "panel",
+    "panic",
+    "paradox",
+    "pardon",
+    "paris",
+    "parker",
+    "parking",
+    "parody",
+    "partner",
+    "passage",
+    "passive",
+    "pasta",
+    "pastel",
+    "patent",
+    "patriot",
+    "patrol",
+    "patron",
+    "pegasus",
+    "pelican",
+    "penguin",
+    "pepper",
+    "percent",
+    "perfect",
+    "perfume",
+    "period",
+    "permit",
+    "person",
+    "peru",
+    "phone",
+    "photo",
+    "piano",
+    "picasso",
+    "picnic",
+    "picture",
+    "pigment",
+    "pilgrim",
+    "pilot",
+    "pirate",
+    "pixel",
+    "pizza",
+    "planet",
+    "plasma",
+    "plaster",
+    "plastic",
+    "plaza",
+    "pocket",
+    "poem",
+    "poetic",
+    "poker",
+    "polaris",
+    "police",
+    "politic",
+    "polo",
+    "polygon",
+    "pony",
+    "popcorn",
+    "popular",
+    "postage",
+    "postal",
+    "precise",
+    "prefix",
+    "premium",
+    "present",
+    "price",
+    "prince",
+    "printer",
+    "prism",
+    "private",
+    "product",
+    "profile",
+    "program",
+    "project",
+    "protect",
+    "proton",
+    "public",
+    "pulse",
+    "puma",
+    "pyramid",
+    "queen",
+    "radar",
+    "radio",
+    "random",
+    "rapid",
+    "rebel",
+    "record",
+    "recycle",
+    "reflex",
+    "reform",
+    "regard",
+    "regular",
+    "relax",
+    "report",
+    "reptile",
+    "reverse",
+    "ricardo",
+    "ringo",
+    "ritual",
+    "robert",
+    "robot",
+    "rocket",
+    "rodeo",
+    "romeo",
+    "royal",
+    "russian",
+    "safari",
+    "salad",
+    "salami",
+    "salmon",
+    "salon",
+    "salute",
+    "samba",
+    "sandra",
+    "santana",
+    "sardine",
+    "school",
+    "screen",
+    "script",
+    "second",
+    "secret",
+    "section",
+    "segment",
+    "select",
+    "seminar",
+    "senator",
+    "senior",
+    "sensor",
+    "serial",
+    "service",
+    "sheriff",
+    "shock",
+    "sierra",
+    "signal",
+    "silicon",
+    "silver",
+    "similar",
+    "simon",
+    "single",
+    "siren",
+    "slogan",
+    "social",
+    "soda",
+    "solar",
+    "solid",
+    "solo",
+    "sonic",
+    "soviet",
+    "special",
+    "speed",
+    "spiral",
+    "spirit",
+    "sport",
+    "static",
+    "station",
+    "status",
+    "stereo",
+    "stone",
+    "stop",
+    "street",
+    "strong",
+    "student",
+    "studio",
+    "style",
+    "subject",
+    "sultan",
+    "super",
+    "susan",
+    "sushi",
+    "suzuki",
+    "switch",
+    "symbol",
+    "system",
+    "tactic",
+    "tahiti",
+    "talent",
+    "tango",
+    "tarzan",
+    "taxi",
+    "telex",
+    "tempo",
+    "tennis",
+    "texas",
+    "textile",
+    "theory",
+    "thermos",
+    "tiger",
+    "titanic",
+    "tokyo",
+    "tomato",
+    "topic",
+    "tornado",
+    "toronto",
+    "torpedo",
+    "total",
+    "totem",
+    "tourist",
+    "tractor",
+    "traffic",
+    "transit",
+    "trapeze",
+    "travel",
+    "tribal",
+    "trick",
+    "trident",
+    "trilogy",
+    "tripod",
+    "tropic",
+    "trumpet",
+    "tulip",
+    "tuna",
+    "turbo",
+    "twist",
+    "ultra",
+    "uniform",
+    "union",
+    "uranium",
+    "vacuum",
+    "valid",
+    "vampire",
+    "vanilla",
+    "vatican",
+    "velvet",
+    "ventura",
+    "venus",
+    "vertigo",
+    "veteran",
+    "victor",
+    "video",
+    "vienna",
+    "viking",
+    "village",
+    "vincent",
+    "violet",
+    "violin",
+    "virtual",
+    "virus",
+    "visa",
+    "vision",
+    "visitor",
+    "visual",
+    "vitamin",
+    "viva",
+    "vocal",
+    "vodka",
+    "volcano",
+    "voltage",
+    "volume",
+    "voyage",
+    "water",
+    "weekend",
+    "welcome",
+    "western",
+    "window",
+    "winter",
+    "wizard",
+    "wolf",
+    "world",
+    "xray",
+    "yankee",
+    "yoga",
+    "yogurt",
+    "yoyo",
+    "zebra",
+    "zero",
+    "zigzag",
+    "zipper",
+    "zodiac",
+    "zoom",
+    "abraham",
+    "action",
+    "address",
+    "alabama",
+    "alfred",
+    "almond",
+    "ammonia",
+    "analyze",
+    "annual",
+    "answer",
+    "apple",
+    "arena",
+    "armada",
+    "arsenal",
+    "atlanta",
+    "atomic",
+    "avenue",
+    "average",
+    "bagel",
+    "baker",
+    "ballet",
+    "bambino",
+    "bamboo",
+    "barbara",
+    "basket",
+    "bazaar",
+    "benefit",
+    "bicycle",
+    "bishop",
+    "blitz",
+    "bonjour",
+    "bottle",
+    "bridge",
+    "british",
+    "brother",
+    "brush",
+    "budget",
+    "cabaret",
+    "cadet",
+    "candle",
+    "capitan",
+    "capsule",
+    "career",
+    "cartoon",
+    "channel",
+    "chapter",
+    "cheese",
+    "circle",
+    "cobalt",
+    "cockpit",
+    "college",
+    "compass",
+    "comrade",
+    "condor",
+    "crimson",
+    "cyclone",
+    "darwin",
+    "declare",
+    "degree",
+    "delete",
+    "delphi",
+    "denver",
+    "desert",
+    "divide",
+    "dolby",
+    "domain",
+    "domingo",
+    "double",
+    "drink",
+    "driver",
+    "eagle",
+    "earth",
+    "echo",
+    "eclipse",
+    "editor",
+    "educate",
+    "edward",
+    "effect",
+    "electra",
+    "emerald",
+    "emotion",
+    "empire",
+    "empty",
+    "escape",
+    "eternal",
+    "evening",
+    "exhibit",
+    "expand",
+    "explore",
+    "extreme",
+    "ferrari",
+    "first",
+    "flag",
+    "folio",
+    "forget",
+    "forward",
+    "freedom",
+    "fresh",
+    "friday",
+    "fuji",
+    "galileo",
+    "garcia",
+    "genesis",
+    "gold",
+    "gravity",
+    "habitat",
+    "hamlet",
+    "harlem",
+    "helium",
+    "holiday",
+    "house",
+    "hunter",
+    "ibiza",
+    "iceberg",
+    "imagine",
+    "infant",
+    "isotope",
+    "jackson",
+    "jamaica",
+    "jasmine",
+    "java",
+    "jessica",
+    "judo",
+    "kitchen",
+    "lazarus",
+    "letter",
+    "license",
+    "lithium",
+    "loyal",
+    "lucky",
+    "magenta",
+    "mailbox",
+    "manual",
+    "marble",
+    "mary",
+    "maxwell",
+    "mayor",
+    "milk",
+    "monarch",
+    "monday",
+    "money",
+    "morning",
+    "mother",
+    "mystery",
+    "native",
+    "nectar",
+    "nelson",
+    "network",
+    "next",
+    "nikita",
+    "nobel",
+    "nobody",
+    "nominal",
+    "norway",
+    "nothing",
+    "number",
+    "october",
+    "office",
+    "oliver",
+    "opinion",
+    "option",
+    "order",
+    "outside",
+    "package",
+    "pancake",
+    "pandora",
+    "panther",
+    "papa",
+    "patient",
+    "pattern",
+    "pedro",
+    "pencil",
+    "people",
+    "phantom",
+    "philips",
+    "pioneer",
+    "pluto",
+    "podium",
+    "portal",
+    "potato",
+    "prize",
+    "process",
+    "protein",
+    "proxy",
+    "pump",
+    "pupil",
+    "python",
+    "quality",
+    "quarter",
+    "quiet",
+    "rabbit",
+    "radical",
+    "radius",
+    "rainbow",
+    "ralph",
+    "ramirez",
+    "ravioli",
+    "raymond",
+    "respect",
+    "respond",
+    "result",
+    "resume",
+    "retro",
+    "richard",
+    "right",
+    "risk",
+    "river",
+    "roger",
+    "roman",
+    "rondo",
+    "sabrina",
+    "salary",
+    "salsa",
+    "sample",
+    "samuel",
+    "saturn",
+    "savage",
+    "scarlet",
+    "scoop",
+    "scorpio",
+    "scratch",
+    "scroll",
+    "sector",
+    "serpent",
+    "shadow",
+    "shampoo",
+    "sharon",
+    "sharp",
+    "short",
+    "shrink",
+    "silence",
+    "silk",
+    "simple",
+    "slang",
+    "smart",
+    "smoke",
+    "snake",
+    "society",
+    "sonar",
+    "sonata",
+    "soprano",
+    "source",
+    "sparta",
+    "sphere",
+    "spider",
+    "sponsor",
+    "spring",
+    "acid",
+    "adios",
+    "agatha",
+    "alamo",
+    "alert",
+    "almanac",
+    "aloha",
+    "andrea",
+    "anita",
+    "arcade",
+    "aurora",
+    "avalon",
+    "baby",
+    "baggage",
+    "balloon",
+    "bank",
+    "basil",
+    "begin",
+    "biscuit",
+    "blue",
+    "bombay",
+    "brain",
+    "brenda",
+    "brigade",
+    "cable",
+    "carmen",
+    "cello",
+    "celtic",
+    "chariot",
+    "chrome",
+    "citrus",
+    "civil",
+    "cloud",
+    "common",
+    "compare",
+    "cool",
+    "copper",
+    "coral",
+    "crater",
+    "cubic",
+    "cupid",
+    "cycle",
+    "depend",
+    "door",
+    "dream",
+    "dynasty",
+    "edison",
+    "edition",
+    "enigma",
+    "equal",
+    "eric",
+    "event",
+    "evita",
+    "exodus",
+    "extend",
+    "famous",
+    "farmer",
+    "food",
+    "fossil",
+    "frog",
+    "fruit",
+    "geneva",
+    "gentle",
+    "george",
+    "giant",
+    "gilbert",
+    "gossip",
+    "gram",
+    "greek",
+    "grille",
+    "hammer",
+    "harvest",
+    "hazard",
+    "heaven",
+    "herbert",
+    "heroic",
+    "hexagon",
+    "husband",
+    "immune",
+    "inca",
+    "inch",
+    "initial",
+    "isabel",
+    "ivory",
+    "jason",
+    "jerome",
+    "joel",
+    "joshua",
+    "journal",
+    "judge",
+    "juliet",
+    "jump",
+    "justice",
+    "kimono",
+    "kinetic",
+    "leonid",
+    "lima",
+    "maze",
+    "medusa",
+    "member",
+    "memphis",
+    "michael",
+    "miguel",
+    "milan",
+    "mile",
+    "miller",
+    "mimic",
+    "mimosa",
+    "mission",
+    "monkey",
+    "moral",
+    "moses",
+    "mouse",
+    "nancy",
+    "natasha",
+    "nebula",
+    "nickel",
+    "nina",
+    "noise",
+    "orchid",
+    "oregano",
+    "origami",
+    "orinoco",
+    "orion",
+    "othello",
+    "paper",
+    "paprika",
+    "prelude",
+    "prepare",
+    "pretend",
+    "profit",
+    "promise",
+    "provide",
+    "puzzle",
+    "remote",
+    "repair",
+    "reply",
+    "rival",
+    "riviera",
+    "robin",
+    "rose",
+    "rover",
+    "rudolf",
+    "saga",
+    "sahara",
+    "scholar",
+    "shelter",
+    "ship",
+    "shoe",
+    "sigma",
+    "sister",
+    "sleep",
+    "smile",
+    "spain",
+    "spark",
+    "split",
+    "spray",
+    "square",
+    "stadium",
+    "star",
+    "storm",
+    "story",
+    "strange",
+    "stretch",
+    "stuart",
+    "subway",
+    "sugar",
+    "sulfur",
+    "summer",
+    "survive",
+    "sweet",
+    "swim",
+    "table",
+    "taboo",
+    "target",
+    "teacher",
+    "telecom",
+    "temple",
+    "tibet",
+    "ticket",
+    "tina",
+    "today",
+    "toga",
+    "tommy",
+    "tower",
+    "trivial",
+    "tunnel",
+    "turtle",
+    "twin",
+    "uncle",
+    "unicorn",
+    "unique",
+    "update",
+    "valery",
+    "vega",
+    "version",
+    "voodoo",
+    "warning",
+    "william",
+    "wonder",
+    "year",
+    "yellow",
+    "young",
+    "absent",
+    "absorb",
+    "accent",
+    "alfonso",
+    "alias",
+    "ambient",
+    "andy",
+    "anvil",
+    "appear",
+    "apropos",
+    "archer",
+    "ariel",
+    "armor",
+    "arrow",
+    "austin",
+    "avatar",
+    "axis",
+    "baboon",
+    "bahama",
+    "bali",
+    "balsa",
+    "bazooka",
+    "beach",
+    "beast",
+    "beatles",
+    "beauty",
+    "before",
+    "benny",
+    "betty",
+    "between",
+    "beyond",
+    "billy",
+    "bison",
+    "blast",
+    "bless",
+    "bogart",
+    "bonanza",
+    "book",
+    "border",
+    "brave",
+    "bread",
+    "break",
+    "broken",
+    "bucket",
+    "buenos",
+    "buffalo",
+    "bundle",
+    "button",
+    "buzzer",
+    "byte",
+    "caesar",
+    "camilla",
+    "canary",
+    "candid",
+    "carrot",
+    "cave",
+    "chant",
+    "child",
+    "choice",
+    "chris",
+    "cipher",
+    "clarion",
+    "clark",
+    "clever",
+    "cliff",
+    "clone",
+    "conan",
+    "conduct",
+    "congo",
+    "content",
+    "costume",
+    "cotton",
+    "cover",
+    "crack",
+    "current",
+    "danube",
+    "data",
+    "decide",
+    "desire",
+    "detail",
+    "dexter",
+    "dinner",
+    "dispute",
+    "donor",
+    "druid",
+    "drum",
+    "easy",
+    "eddie",
+    "enjoy",
+    "enrico",
+    "epoxy",
+    "erosion",
+    "except",
+    "exile",
+    "explain",
+    "fame",
+    "fast",
+    "father",
+    "felix",
+    "field",
+    "fiona",
+    "fire",
+    "fish",
+    "flame",
+    "flex",
+    "flipper",
+    "float",
+    "flood",
+    "floor",
+    "forbid",
+    "forever",
+    "fractal",
+    "frame",
+    "freddie",
+    "front",
+    "fuel",
+    "gallop",
+    "game",
+    "garbo",
+    "gate",
+    "gibson",
+    "ginger",
+    "giraffe",
+    "gizmo",
+    "glass",
+    "goblin",
+    "gopher",
+    "grace",
+    "gray",
+    "gregory",
+    "grid",
+    "griffin",
+    "ground",
+    "guest",
+    "gustav",
+    "gyro",
+    "hair",
+    "halt",
+    "harris",
+    "heart",
+    "heavy",
+    "herman",
+    "hippie",
+    "hobby",
+    "honey",
+    "hope",
+    "horse",
+    "hostel",
+    "hydro",
+    "imitate",
+    "info",
+    "ingrid",
+    "inside",
+    "invent",
+    "invest",
+    "invite",
+    "iron",
+    "ivan",
+    "james",
+    "jester",
+    "jimmy",
+    "join",
+    "joseph",
+    "juice",
+    "julius",
+    "july",
+    "justin",
+    "kansas",
+    "karl",
+    "kevin",
+    "kiwi",
+    "ladder",
+    "lake",
+    "laura",
+    "learn",
+    "legacy",
+    "legend",
+    "lesson",
+    "life",
+    "light",
+    "list",
+    "locate",
+    "lopez",
+    "lorenzo",
+    "love",
+    "lunch",
+    "malta",
+    "mammal",
+    "margo",
+    "marion",
+    "mask",
+    "match",
+    "mayday",
+    "meaning",
+    "mercy",
+    "middle",
+    "mike",
+    "mirror",
+    "modest",
+    "morph",
+    "morris",
+    "nadia",
+    "nato",
+    "navy",
+    "needle",
+    "neuron",
+    "never",
+    "newton",
+    "nice",
+    "night",
+    "nissan",
+    "nitro",
+    "nixon",
+    "north",
+    "oberon",
+    "octavia",
+    "ohio",
+    "olga",
+    "open",
+    "opus",
+    "orca",
+    "oval",
+    "owner",
+    "page",
+    "paint",
+    "palma",
+    "parade",
+    "parent",
+    "parole",
+    "paul",
+    "peace",
+    "pearl",
+    "perform",
+    "phoenix",
+    "phrase",
+    "pierre",
+    "pinball",
+    "place",
+    "plate",
+    "plato",
+    "plume",
+    "pogo",
+    "point",
+    "polite",
+    "polka",
+    "poncho",
+    "powder",
+    "prague",
+    "press",
+    "presto",
+    "pretty",
+    "prime",
+    "promo",
+    "quasi",
+    "quest",
+    "quick",
+    "quiz",
+    "quota",
+    "race",
+    "rachel",
+    "raja",
+    "ranger",
+    "region",
+    "remark",
+    "rent",
+    "reward",
+    "rhino",
+    "ribbon",
+    "rider",
+    "road",
+    "rodent",
+    "round",
+    "rubber",
+    "ruby",
+    "rufus",
+    "sabine",
+    "saddle",
+    "sailor",
+    "saint",
+    "salt",
+    "satire",
+    "scale",
+    "scuba",
+    "season",
+    "secure",
+    "shake",
+    "shallow",
+    "shannon",
+    "shave",
+    "shelf",
+    "sherman",
+    "shine",
+    "shirt",
+    "side",
+    "sinatra",
+    "sincere",
+    "size",
+    "slalom",
+    "slow",
+    "small",
+    "snow",
+    "sofia",
+    "song",
+    "sound",
+    "south",
+    "speech",
+    "spell",
+    "spend",
+    "spoon",
+    "stage",
+    "stamp",
+    "stand",
+    "state",
+    "stella",
+    "stick",
+    "sting",
+    "stock",
+    "store",
+    "sunday",
+    "sunset",
+    "support",
+    "sweden",
+    "swing",
+    "tape",
+    "think",
+    "thomas",
+    "tictac",
+    "time",
+    "toast",
+    "tobacco",
+    "tonight",
+    "torch",
+    "torso",
+    "touch",
+    "toyota",
+    "trade",
+    "tribune",
+    "trinity",
+    "triton",
+    "truck",
+    "trust",
+    "type",
+    "under",
+    "unit",
+    "urban",
+    "urgent",
+    "user",
+    "value",
+    "vendor",
+    "venice",
+    "verona",
+    "vibrate",
+    "virgo",
+    "visible",
+    "vista",
+    "vital",
+    "voice",
+    "vortex",
+    "waiter",
+    "watch",
+    "wave",
+    "weather",
+    "wedding",
+    "wheel",
+    "whiskey",
+    "wisdom",
+    "deal",
+    "null",
+    "nurse",
+    "quebec",
+    "reserve",
+    "reunion",
+    "roof",
+    "singer",
+    "verbal",
+    "amen",
+    "ego",
+    "fax",
+    "jet",
+    "job",
+    "rio",
+    "ski",
+    "yes"
+  ]
+}

+ 2 - 0
rebuild/.gitignore

@@ -0,0 +1,2 @@
+mnemonicode/
+__pycache__/

+ 110 - 0
rebuild/build_inline.py

@@ -0,0 +1,110 @@
+#!/usr/bin/env python3
+"""
+Build an inlined variant of mnemonicode.py with the wordlist embedded as a
+Python literal instead of loaded from wordlist.json at runtime.
+
+Usage:
+  python3 build_inline.py [source.py] [wordlist.json] [output.py]
+
+Defaults:
+  source      mnemonicode.py
+  wordlist    wordlist.json
+  output      mnemonicode_inline.py
+"""
+
+import json
+import sys
+import textwrap
+from pathlib import Path
+
+COLS = 79  # target line length for the word list
+
+# Markers that delimit the section to replace in the source file.
+IMPORT_JSON   = "import json\n"
+IMPORT_PATH   = "from pathlib import Path\n"
+WORDLIST_LINE = "_WORDLIST_JSON = Path(__file__).parent / \"wordlist.json\"\n"
+LOAD_OLD = '''\
+@lru_cache(maxsize=1)
+def _load() -> tuple[list[str], dict[str, int], str]:
+  data = json.loads(_WORDLIST_JSON.read_text())
+  words: list[str] = data["words"]
+  index: dict[str, int] = {w: i for i, w in enumerate(words)}
+  return words, index, data["version"]
+'''
+LOAD_NEW_TMPL = '''\
+_WORDLIST: tuple[list[str], str] = (
+{words_literal},
+  {version_literal},
+)
+
+
+@lru_cache(maxsize=1)
+def _load() -> tuple[list[str], dict[str, int], str]:
+  words, version = _WORDLIST
+  index: dict[str, int] = {{w: i for i, w in enumerate(words)}}
+  return words, index, version
+'''
+
+
+def format_words(words: list[str]) -> str:
+  """Format a list of words as a Python list literal, wrapped at COLS."""
+  items = [repr(w) for w in words]
+  joined = ", ".join(items)
+  # Fits on one line?
+  one_line = f"  [{joined}]"
+  if len(one_line) <= COLS:
+    return one_line
+  # Wrap into multiple lines indented by 4 spaces inside the outer 2-space indent.
+  lines = []
+  current = ""
+  for item in items:
+    candidate = f"{current}, {item}" if current else item
+    if len(candidate) + 4 > COLS - 2:  # account for "  [" prefix on first line
+      if current:
+        lines.append(current)
+      current = item
+    else:
+      current = candidate
+  if current:
+    lines.append(current)
+  inner = ",\n    ".join(lines)
+  return f"  [\n    {inner},\n  ]"
+
+
+def build(source: Path, wordlist: Path, output: Path) -> None:
+  src = source.read_text()
+  data = json.loads(wordlist.read_text())
+  words: list[str] = data["words"]
+  version: str = data["version"]
+
+  # Remove imports that are only needed for file-based loading.
+  src = src.replace(IMPORT_JSON, "")
+  src = src.replace(IMPORT_PATH, "")
+  src = src.replace(WORDLIST_LINE + "\n", "")  # remove trailing blank line too
+
+  # Replace _load().
+  words_lit = format_words(words)
+  version_lit = repr(version)
+  load_new = LOAD_NEW_TMPL.format(
+    words_literal=words_lit,
+    version_literal=version_lit,
+  )
+  if LOAD_OLD not in src:
+    sys.exit(f"error: could not find _load() definition in {source}")
+  src = src.replace(LOAD_OLD, load_new)
+
+  output.write_text(src)
+  print(f"wrote {output}  ({output.stat().st_size:,} bytes)")
+
+
+if __name__ == "__main__":
+  args = sys.argv[1:]
+  source   = Path(args[0]) if len(args) > 0 else Path("mnemonicode.py")
+  wordlist = Path(args[1]) if len(args) > 1 else Path("wordlist.json")
+  output   = Path(args[2]) if len(args) > 2 else Path("mnemonicode_inline.py")
+
+  for p in (source, wordlist):
+    if not p.exists():
+      sys.exit(f"error: {p} not found")
+
+  build(source, wordlist, output)

+ 1 - 0
rebuild/mndecode.py

@@ -0,0 +1 @@
+mnemonicode.py

+ 1 - 0
rebuild/mndecode_inline.py

@@ -0,0 +1 @@
+mnemonicode_inline.py

+ 122 - 0
rebuild/mnemonicode.py

@@ -0,0 +1,122 @@
+"""
+Pure-Python implementation of mnemonicode encoding/decoding.
+
+Mnemonicode encodes arbitrary bytes as a sequence of human-readable words.
+Each group of 4 bytes maps to 3 words; shorter trailing groups use fewer words.
+The 24-bit trailing case (3 bytes) uses a distinct set of 7 short words to
+signal that a remainder word ends the group.
+
+Word list: version 0.7, 1626 base words + 7 remainder words = 1633 total.
+"""
+
+import json
+import sys
+from functools import lru_cache
+from pathlib import Path
+
+MN_BASE = 1626
+
+_WORDLIST_JSON = Path(__file__).parent / "wordlist.json"
+
+
+@lru_cache(maxsize=1)
+def _load() -> tuple[list[str], dict[str, int], str]:
+  data = json.loads(_WORDLIST_JSON.read_text())
+  words: list[str] = data["words"]
+  index: dict[str, int] = {w: i for i, w in enumerate(words)}
+  return words, index, data["version"]
+
+
+def wordlist_version() -> str:
+  return _load()[2]
+
+
+def words_required(size: int) -> int:
+  """Number of mnemonic words needed to encode `size` bytes."""
+  return ((size + 1) * 3) // 4
+
+
+def encode(data: bytes) -> list[str]:
+  """Encode bytes to a list of mnemonic words."""
+  words, _, _ = _load()
+  result = []
+  for n in range(words_required(len(data))):
+    grp = (n // 3) * 4
+    remaining = min(len(data) - grp, 4)
+    x = int.from_bytes(data[grp:grp + remaining], "little")
+    pos = n % 3
+    extra = 0
+    if pos == 2:
+      if remaining == 3:
+        extra = MN_BASE  # use one of the 7 short remainder words
+      x //= MN_BASE * MN_BASE
+    elif pos == 1:
+      x //= MN_BASE
+    result.append(words[x % MN_BASE + extra])
+  return result
+
+
+def decode(encoded: list[str]) -> bytes:
+  """Decode a list of mnemonic words back to bytes."""
+  _, word_index, _ = _load()
+
+  buf = bytearray(((len(encoded) // 3) + 1) * 4)
+  offset = 0
+
+  for word in encoded:
+    try:
+      idx = word_index[word.lower()]
+    except KeyError:
+      raise ValueError(f"unrecognized word: {word!r}") from None
+
+    grp = offset & ~3
+    x = int.from_bytes(buf[grp:grp + 4], "little")
+    pos = offset % 4
+
+    if pos == 3:
+      raise ValueError("unexpected word after 24-bit remainder")
+    elif pos == 0:
+      x = idx
+      offset += 1
+    elif pos == 1:
+      x += idx * MN_BASE
+      offset += 1
+    elif pos == 2:
+      if idx >= MN_BASE:  # 24-bit remainder word
+        x += (idx - MN_BASE) * MN_BASE * MN_BASE
+        offset += 1  # offset % 4 == 3 signals end-of-group next
+      else:
+        if idx > 1624 or (idx == 1624 and x > 1_312_671):
+          raise ValueError("invalid encoding: value out of range")
+        x += idx * MN_BASE * MN_BASE
+        offset += 2  # skip to next group
+
+    buf[grp:grp + 4] = x.to_bytes(4, "little")
+
+  return bytes(buf[:offset])
+
+
+if __name__ == "__main__":
+  import argparse
+
+  mode = "decode" if "decode" in sys.argv[0] else "encode"
+
+  parser = argparse.ArgumentParser(
+    prog=sys.argv[0],
+    description="Decode mnemonic words to bytes." if mode == "decode" else "Encode bytes to mnemonic words.",
+  )
+  parser.add_argument("-s", "--separator", default="-", help="word separator (default: -)")
+
+  args = parser.parse_args()
+
+  if mode == "encode":
+    data = sys.stdin.buffer.read()
+    print(args.separator.join(encode(data)))
+  else:
+    words = [w.strip() for w in sys.stdin.read().split(args.separator) if w.strip()]
+    try:
+      data = decode(words)
+    except ValueError as e:
+      print(f"error: {e}", file=sys.stderr)
+      sys.exit(1)
+    sys.stdout.buffer.write(data)

+ 350 - 0
rebuild/mnemonicode_inline.py

@@ -0,0 +1,350 @@
+"""
+Pure-Python implementation of mnemonicode encoding/decoding.
+
+Mnemonicode encodes arbitrary bytes as a sequence of human-readable words.
+Each group of 4 bytes maps to 3 words; shorter trailing groups use fewer words.
+The 24-bit trailing case (3 bytes) uses a distinct set of 7 short words to
+signal that a remainder word ends the group.
+
+Word list: version 0.7, 1626 base words + 7 remainder words = 1633 total.
+"""
+
+import sys
+from functools import lru_cache
+
+MN_BASE = 1626
+
+
+_WORDLIST: tuple[list[str], str] = (
+  [
+    'academy', 'acrobat', 'active', 'actor', 'adam', 'admiral', 'adrian',
+    'africa', 'agenda', 'agent', 'airline', 'airport', 'aladdin', 'alarm',
+    'alaska', 'albert', 'albino', 'album', 'alcohol', 'alex', 'algebra',
+    'alibi', 'alice', 'alien', 'alpha', 'alpine', 'amadeus', 'amanda',
+    'amazon', 'amber', 'america', 'amigo', 'analog', 'anatomy', 'angel',
+    'animal', 'antenna', 'antonio', 'apollo', 'april', 'archive', 'arctic',
+    'arizona', 'arnold', 'aroma', 'arthur', 'artist', 'asia', 'aspect',
+    'aspirin', 'athena', 'athlete', 'atlas', 'audio', 'august', 'austria',
+    'axiom', 'aztec', 'balance', 'ballad', 'banana', 'bandit', 'banjo',
+    'barcode', 'baron', 'basic', 'battery', 'belgium', 'berlin', 'bermuda',
+    'bernard', 'bikini', 'binary', 'bingo', 'biology', 'block', 'blonde',
+    'bonus', 'boris', 'boston', 'boxer', 'brandy', 'bravo', 'brazil',
+    'bronze', 'brown', 'bruce', 'bruno', 'burger', 'burma', 'cabinet',
+    'cactus', 'cafe', 'cairo', 'cake', 'calypso', 'camel', 'camera', 'campus',
+    'canada', 'canal', 'cannon', 'canoe', 'cantina', 'canvas', 'canyon',
+    'capital', 'caramel', 'caravan', 'carbon', 'cargo', 'carlo', 'carol',
+    'carpet', 'cartel', 'casino', 'castle', 'castro', 'catalog', 'caviar',
+    'cecilia', 'cement', 'center', 'century', 'ceramic', 'chamber', 'chance',
+    'change', 'chaos', 'charlie', 'charm', 'charter', 'chef', 'chemist',
+    'cherry', 'chess', 'chicago', 'chicken', 'chief', 'china', 'cigar',
+    'cinema', 'circus', 'citizen', 'city', 'clara', 'classic', 'claudia',
+    'clean', 'client', 'climax', 'clinic', 'clock', 'club', 'cobra',
+    'coconut', 'cola', 'collect', 'colombo', 'colony', 'color', 'combat',
+    'comedy', 'comet', 'command', 'compact', 'company', 'complex', 'concept',
+    'concert', 'connect', 'consul', 'contact', 'context', 'contour',
+    'control', 'convert', 'copy', 'corner', 'corona', 'correct', 'cosmos',
+    'couple', 'courage', 'cowboy', 'craft', 'crash', 'credit', 'cricket',
+    'critic', 'crown', 'crystal', 'cuba', 'culture', 'dallas', 'dance',
+    'daniel', 'david', 'decade', 'decimal', 'deliver', 'delta', 'deluxe',
+    'demand', 'demo', 'denmark', 'derby', 'design', 'detect', 'develop',
+    'diagram', 'dialog', 'diamond', 'diana', 'diego', 'diesel', 'diet',
+    'digital', 'dilemma', 'diploma', 'direct', 'disco', 'disney', 'distant',
+    'doctor', 'dollar', 'dominic', 'domino', 'donald', 'dragon', 'drama',
+    'dublin', 'duet', 'dynamic', 'east', 'ecology', 'economy', 'edgar',
+    'egypt', 'elastic', 'elegant', 'element', 'elite', 'elvis', 'email',
+    'energy', 'engine', 'english', 'episode', 'equator', 'escort', 'ethnic',
+    'europe', 'everest', 'evident', 'exact', 'example', 'exit', 'exotic',
+    'export', 'express', 'extra', 'fabric', 'factor', 'falcon', 'family',
+    'fantasy', 'fashion', 'fiber', 'fiction', 'fidel', 'fiesta', 'figure',
+    'film', 'filter', 'final', 'finance', 'finish', 'finland', 'flash',
+    'florida', 'flower', 'fluid', 'flute', 'focus', 'ford', 'forest',
+    'formal', 'format', 'formula', 'fortune', 'forum', 'fragile', 'france',
+    'frank', 'friend', 'frozen', 'future', 'gabriel', 'galaxy', 'gallery',
+    'gamma', 'garage', 'garden', 'garlic', 'gemini', 'general', 'genetic',
+    'genius', 'germany', 'global', 'gloria', 'golf', 'gondola', 'gong',
+    'good', 'gordon', 'gorilla', 'grand', 'granite', 'graph', 'green',
+    'group', 'guide', 'guitar', 'guru', 'hand', 'happy', 'harbor', 'harmony',
+    'harvard', 'havana', 'hawaii', 'helena', 'hello', 'henry', 'hilton',
+    'history', 'horizon', 'hotel', 'human', 'humor', 'icon', 'idea', 'igloo',
+    'igor', 'image', 'impact', 'import', 'index', 'india', 'indigo', 'input',
+    'insect', 'instant', 'iris', 'italian', 'jacket', 'jacob', 'jaguar',
+    'janet', 'japan', 'jargon', 'jazz', 'jeep', 'john', 'joker', 'jordan',
+    'jumbo', 'june', 'jungle', 'junior', 'jupiter', 'karate', 'karma',
+    'kayak', 'kermit', 'kilo', 'king', 'koala', 'korea', 'labor', 'lady',
+    'lagoon', 'laptop', 'laser', 'latin', 'lava', 'lecture', 'left', 'legal',
+    'lemon', 'level', 'lexicon', 'liberal', 'libra', 'limbo', 'limit',
+    'linda', 'linear', 'lion', 'liquid', 'liter', 'little', 'llama', 'lobby',
+    'lobster', 'local', 'logic', 'logo', 'lola', 'london', 'lotus', 'lucas',
+    'lunar', 'machine', 'macro', 'madam', 'madonna', 'madrid', 'maestro',
+    'magic', 'magnet', 'magnum', 'major', 'mama', 'mambo', 'manager', 'mango',
+    'manila', 'marco', 'marina', 'market', 'mars', 'martin', 'marvin',
+    'master', 'matrix', 'maximum', 'media', 'medical', 'mega', 'melody',
+    'melon', 'memo', 'mental', 'mentor', 'menu', 'mercury', 'message',
+    'metal', 'meteor', 'meter', 'method', 'metro', 'mexico', 'miami', 'micro',
+    'million', 'mineral', 'minimum', 'minus', 'minute', 'miracle', 'mirage',
+    'miranda', 'mister', 'mixer', 'mobile', 'model', 'modem', 'modern',
+    'modular', 'moment', 'monaco', 'monica', 'monitor', 'mono', 'monster',
+    'montana', 'morgan', 'motel', 'motif', 'motor', 'mozart', 'multi',
+    'museum', 'music', 'mustang', 'natural', 'neon', 'nepal', 'neptune',
+    'nerve', 'neutral', 'nevada', 'news', 'ninja', 'nirvana', 'normal',
+    'nova', 'novel', 'nuclear', 'numeric', 'nylon', 'oasis', 'object',
+    'observe', 'ocean', 'octopus', 'olivia', 'olympic', 'omega', 'opera',
+    'optic', 'optimal', 'orange', 'orbit', 'organic', 'orient', 'origin',
+    'orlando', 'oscar', 'oxford', 'oxygen', 'ozone', 'pablo', 'pacific',
+    'pagoda', 'palace', 'pamela', 'panama', 'panda', 'panel', 'panic',
+    'paradox', 'pardon', 'paris', 'parker', 'parking', 'parody', 'partner',
+    'passage', 'passive', 'pasta', 'pastel', 'patent', 'patriot', 'patrol',
+    'patron', 'pegasus', 'pelican', 'penguin', 'pepper', 'percent', 'perfect',
+    'perfume', 'period', 'permit', 'person', 'peru', 'phone', 'photo',
+    'piano', 'picasso', 'picnic', 'picture', 'pigment', 'pilgrim', 'pilot',
+    'pirate', 'pixel', 'pizza', 'planet', 'plasma', 'plaster', 'plastic',
+    'plaza', 'pocket', 'poem', 'poetic', 'poker', 'polaris', 'police',
+    'politic', 'polo', 'polygon', 'pony', 'popcorn', 'popular', 'postage',
+    'postal', 'precise', 'prefix', 'premium', 'present', 'price', 'prince',
+    'printer', 'prism', 'private', 'product', 'profile', 'program', 'project',
+    'protect', 'proton', 'public', 'pulse', 'puma', 'pyramid', 'queen',
+    'radar', 'radio', 'random', 'rapid', 'rebel', 'record', 'recycle',
+    'reflex', 'reform', 'regard', 'regular', 'relax', 'report', 'reptile',
+    'reverse', 'ricardo', 'ringo', 'ritual', 'robert', 'robot', 'rocket',
+    'rodeo', 'romeo', 'royal', 'russian', 'safari', 'salad', 'salami',
+    'salmon', 'salon', 'salute', 'samba', 'sandra', 'santana', 'sardine',
+    'school', 'screen', 'script', 'second', 'secret', 'section', 'segment',
+    'select', 'seminar', 'senator', 'senior', 'sensor', 'serial', 'service',
+    'sheriff', 'shock', 'sierra', 'signal', 'silicon', 'silver', 'similar',
+    'simon', 'single', 'siren', 'slogan', 'social', 'soda', 'solar', 'solid',
+    'solo', 'sonic', 'soviet', 'special', 'speed', 'spiral', 'spirit',
+    'sport', 'static', 'station', 'status', 'stereo', 'stone', 'stop',
+    'street', 'strong', 'student', 'studio', 'style', 'subject', 'sultan',
+    'super', 'susan', 'sushi', 'suzuki', 'switch', 'symbol', 'system',
+    'tactic', 'tahiti', 'talent', 'tango', 'tarzan', 'taxi', 'telex', 'tempo',
+    'tennis', 'texas', 'textile', 'theory', 'thermos', 'tiger', 'titanic',
+    'tokyo', 'tomato', 'topic', 'tornado', 'toronto', 'torpedo', 'total',
+    'totem', 'tourist', 'tractor', 'traffic', 'transit', 'trapeze', 'travel',
+    'tribal', 'trick', 'trident', 'trilogy', 'tripod', 'tropic', 'trumpet',
+    'tulip', 'tuna', 'turbo', 'twist', 'ultra', 'uniform', 'union', 'uranium',
+    'vacuum', 'valid', 'vampire', 'vanilla', 'vatican', 'velvet', 'ventura',
+    'venus', 'vertigo', 'veteran', 'victor', 'video', 'vienna', 'viking',
+    'village', 'vincent', 'violet', 'violin', 'virtual', 'virus', 'visa',
+    'vision', 'visitor', 'visual', 'vitamin', 'viva', 'vocal', 'vodka',
+    'volcano', 'voltage', 'volume', 'voyage', 'water', 'weekend', 'welcome',
+    'western', 'window', 'winter', 'wizard', 'wolf', 'world', 'xray',
+    'yankee', 'yoga', 'yogurt', 'yoyo', 'zebra', 'zero', 'zigzag', 'zipper',
+    'zodiac', 'zoom', 'abraham', 'action', 'address', 'alabama', 'alfred',
+    'almond', 'ammonia', 'analyze', 'annual', 'answer', 'apple', 'arena',
+    'armada', 'arsenal', 'atlanta', 'atomic', 'avenue', 'average', 'bagel',
+    'baker', 'ballet', 'bambino', 'bamboo', 'barbara', 'basket', 'bazaar',
+    'benefit', 'bicycle', 'bishop', 'blitz', 'bonjour', 'bottle', 'bridge',
+    'british', 'brother', 'brush', 'budget', 'cabaret', 'cadet', 'candle',
+    'capitan', 'capsule', 'career', 'cartoon', 'channel', 'chapter', 'cheese',
+    'circle', 'cobalt', 'cockpit', 'college', 'compass', 'comrade', 'condor',
+    'crimson', 'cyclone', 'darwin', 'declare', 'degree', 'delete', 'delphi',
+    'denver', 'desert', 'divide', 'dolby', 'domain', 'domingo', 'double',
+    'drink', 'driver', 'eagle', 'earth', 'echo', 'eclipse', 'editor',
+    'educate', 'edward', 'effect', 'electra', 'emerald', 'emotion', 'empire',
+    'empty', 'escape', 'eternal', 'evening', 'exhibit', 'expand', 'explore',
+    'extreme', 'ferrari', 'first', 'flag', 'folio', 'forget', 'forward',
+    'freedom', 'fresh', 'friday', 'fuji', 'galileo', 'garcia', 'genesis',
+    'gold', 'gravity', 'habitat', 'hamlet', 'harlem', 'helium', 'holiday',
+    'house', 'hunter', 'ibiza', 'iceberg', 'imagine', 'infant', 'isotope',
+    'jackson', 'jamaica', 'jasmine', 'java', 'jessica', 'judo', 'kitchen',
+    'lazarus', 'letter', 'license', 'lithium', 'loyal', 'lucky', 'magenta',
+    'mailbox', 'manual', 'marble', 'mary', 'maxwell', 'mayor', 'milk',
+    'monarch', 'monday', 'money', 'morning', 'mother', 'mystery', 'native',
+    'nectar', 'nelson', 'network', 'next', 'nikita', 'nobel', 'nobody',
+    'nominal', 'norway', 'nothing', 'number', 'october', 'office', 'oliver',
+    'opinion', 'option', 'order', 'outside', 'package', 'pancake', 'pandora',
+    'panther', 'papa', 'patient', 'pattern', 'pedro', 'pencil', 'people',
+    'phantom', 'philips', 'pioneer', 'pluto', 'podium', 'portal', 'potato',
+    'prize', 'process', 'protein', 'proxy', 'pump', 'pupil', 'python',
+    'quality', 'quarter', 'quiet', 'rabbit', 'radical', 'radius', 'rainbow',
+    'ralph', 'ramirez', 'ravioli', 'raymond', 'respect', 'respond', 'result',
+    'resume', 'retro', 'richard', 'right', 'risk', 'river', 'roger', 'roman',
+    'rondo', 'sabrina', 'salary', 'salsa', 'sample', 'samuel', 'saturn',
+    'savage', 'scarlet', 'scoop', 'scorpio', 'scratch', 'scroll', 'sector',
+    'serpent', 'shadow', 'shampoo', 'sharon', 'sharp', 'short', 'shrink',
+    'silence', 'silk', 'simple', 'slang', 'smart', 'smoke', 'snake',
+    'society', 'sonar', 'sonata', 'soprano', 'source', 'sparta', 'sphere',
+    'spider', 'sponsor', 'spring', 'acid', 'adios', 'agatha', 'alamo',
+    'alert', 'almanac', 'aloha', 'andrea', 'anita', 'arcade', 'aurora',
+    'avalon', 'baby', 'baggage', 'balloon', 'bank', 'basil', 'begin',
+    'biscuit', 'blue', 'bombay', 'brain', 'brenda', 'brigade', 'cable',
+    'carmen', 'cello', 'celtic', 'chariot', 'chrome', 'citrus', 'civil',
+    'cloud', 'common', 'compare', 'cool', 'copper', 'coral', 'crater',
+    'cubic', 'cupid', 'cycle', 'depend', 'door', 'dream', 'dynasty', 'edison',
+    'edition', 'enigma', 'equal', 'eric', 'event', 'evita', 'exodus',
+    'extend', 'famous', 'farmer', 'food', 'fossil', 'frog', 'fruit', 'geneva',
+    'gentle', 'george', 'giant', 'gilbert', 'gossip', 'gram', 'greek',
+    'grille', 'hammer', 'harvest', 'hazard', 'heaven', 'herbert', 'heroic',
+    'hexagon', 'husband', 'immune', 'inca', 'inch', 'initial', 'isabel',
+    'ivory', 'jason', 'jerome', 'joel', 'joshua', 'journal', 'judge',
+    'juliet', 'jump', 'justice', 'kimono', 'kinetic', 'leonid', 'lima',
+    'maze', 'medusa', 'member', 'memphis', 'michael', 'miguel', 'milan',
+    'mile', 'miller', 'mimic', 'mimosa', 'mission', 'monkey', 'moral',
+    'moses', 'mouse', 'nancy', 'natasha', 'nebula', 'nickel', 'nina', 'noise',
+    'orchid', 'oregano', 'origami', 'orinoco', 'orion', 'othello', 'paper',
+    'paprika', 'prelude', 'prepare', 'pretend', 'profit', 'promise',
+    'provide', 'puzzle', 'remote', 'repair', 'reply', 'rival', 'riviera',
+    'robin', 'rose', 'rover', 'rudolf', 'saga', 'sahara', 'scholar',
+    'shelter', 'ship', 'shoe', 'sigma', 'sister', 'sleep', 'smile', 'spain',
+    'spark', 'split', 'spray', 'square', 'stadium', 'star', 'storm', 'story',
+    'strange', 'stretch', 'stuart', 'subway', 'sugar', 'sulfur', 'summer',
+    'survive', 'sweet', 'swim', 'table', 'taboo', 'target', 'teacher',
+    'telecom', 'temple', 'tibet', 'ticket', 'tina', 'today', 'toga', 'tommy',
+    'tower', 'trivial', 'tunnel', 'turtle', 'twin', 'uncle', 'unicorn',
+    'unique', 'update', 'valery', 'vega', 'version', 'voodoo', 'warning',
+    'william', 'wonder', 'year', 'yellow', 'young', 'absent', 'absorb',
+    'accent', 'alfonso', 'alias', 'ambient', 'andy', 'anvil', 'appear',
+    'apropos', 'archer', 'ariel', 'armor', 'arrow', 'austin', 'avatar',
+    'axis', 'baboon', 'bahama', 'bali', 'balsa', 'bazooka', 'beach', 'beast',
+    'beatles', 'beauty', 'before', 'benny', 'betty', 'between', 'beyond',
+    'billy', 'bison', 'blast', 'bless', 'bogart', 'bonanza', 'book', 'border',
+    'brave', 'bread', 'break', 'broken', 'bucket', 'buenos', 'buffalo',
+    'bundle', 'button', 'buzzer', 'byte', 'caesar', 'camilla', 'canary',
+    'candid', 'carrot', 'cave', 'chant', 'child', 'choice', 'chris', 'cipher',
+    'clarion', 'clark', 'clever', 'cliff', 'clone', 'conan', 'conduct',
+    'congo', 'content', 'costume', 'cotton', 'cover', 'crack', 'current',
+    'danube', 'data', 'decide', 'desire', 'detail', 'dexter', 'dinner',
+    'dispute', 'donor', 'druid', 'drum', 'easy', 'eddie', 'enjoy', 'enrico',
+    'epoxy', 'erosion', 'except', 'exile', 'explain', 'fame', 'fast',
+    'father', 'felix', 'field', 'fiona', 'fire', 'fish', 'flame', 'flex',
+    'flipper', 'float', 'flood', 'floor', 'forbid', 'forever', 'fractal',
+    'frame', 'freddie', 'front', 'fuel', 'gallop', 'game', 'garbo', 'gate',
+    'gibson', 'ginger', 'giraffe', 'gizmo', 'glass', 'goblin', 'gopher',
+    'grace', 'gray', 'gregory', 'grid', 'griffin', 'ground', 'guest',
+    'gustav', 'gyro', 'hair', 'halt', 'harris', 'heart', 'heavy', 'herman',
+    'hippie', 'hobby', 'honey', 'hope', 'horse', 'hostel', 'hydro', 'imitate',
+    'info', 'ingrid', 'inside', 'invent', 'invest', 'invite', 'iron', 'ivan',
+    'james', 'jester', 'jimmy', 'join', 'joseph', 'juice', 'julius', 'july',
+    'justin', 'kansas', 'karl', 'kevin', 'kiwi', 'ladder', 'lake', 'laura',
+    'learn', 'legacy', 'legend', 'lesson', 'life', 'light', 'list', 'locate',
+    'lopez', 'lorenzo', 'love', 'lunch', 'malta', 'mammal', 'margo', 'marion',
+    'mask', 'match', 'mayday', 'meaning', 'mercy', 'middle', 'mike', 'mirror',
+    'modest', 'morph', 'morris', 'nadia', 'nato', 'navy', 'needle', 'neuron',
+    'never', 'newton', 'nice', 'night', 'nissan', 'nitro', 'nixon', 'north',
+    'oberon', 'octavia', 'ohio', 'olga', 'open', 'opus', 'orca', 'oval',
+    'owner', 'page', 'paint', 'palma', 'parade', 'parent', 'parole', 'paul',
+    'peace', 'pearl', 'perform', 'phoenix', 'phrase', 'pierre', 'pinball',
+    'place', 'plate', 'plato', 'plume', 'pogo', 'point', 'polite', 'polka',
+    'poncho', 'powder', 'prague', 'press', 'presto', 'pretty', 'prime',
+    'promo', 'quasi', 'quest', 'quick', 'quiz', 'quota', 'race', 'rachel',
+    'raja', 'ranger', 'region', 'remark', 'rent', 'reward', 'rhino', 'ribbon',
+    'rider', 'road', 'rodent', 'round', 'rubber', 'ruby', 'rufus', 'sabine',
+    'saddle', 'sailor', 'saint', 'salt', 'satire', 'scale', 'scuba', 'season',
+    'secure', 'shake', 'shallow', 'shannon', 'shave', 'shelf', 'sherman',
+    'shine', 'shirt', 'side', 'sinatra', 'sincere', 'size', 'slalom', 'slow',
+    'small', 'snow', 'sofia', 'song', 'sound', 'south', 'speech', 'spell',
+    'spend', 'spoon', 'stage', 'stamp', 'stand', 'state', 'stella', 'stick',
+    'sting', 'stock', 'store', 'sunday', 'sunset', 'support', 'sweden',
+    'swing', 'tape', 'think', 'thomas', 'tictac', 'time', 'toast', 'tobacco',
+    'tonight', 'torch', 'torso', 'touch', 'toyota', 'trade', 'tribune',
+    'trinity', 'triton', 'truck', 'trust', 'type', 'under', 'unit', 'urban',
+    'urgent', 'user', 'value', 'vendor', 'venice', 'verona', 'vibrate',
+    'virgo', 'visible', 'vista', 'vital', 'voice', 'vortex', 'waiter',
+    'watch', 'wave', 'weather', 'wedding', 'wheel', 'whiskey', 'wisdom',
+    'deal', 'null', 'nurse', 'quebec', 'reserve', 'reunion', 'roof', 'singer',
+    'verbal', 'amen', 'ego', 'fax', 'jet', 'job', 'rio', 'ski', 'yes',
+  ],
+  '0.7',
+)
+
+
+@lru_cache(maxsize=1)
+def _load() -> tuple[list[str], dict[str, int], str]:
+  words, version = _WORDLIST
+  index: dict[str, int] = {w: i for i, w in enumerate(words)}
+  return words, index, version
+
+
+def wordlist_version() -> str:
+  return _load()[2]
+
+
+def words_required(size: int) -> int:
+  """Number of mnemonic words needed to encode `size` bytes."""
+  return ((size + 1) * 3) // 4
+
+
+def encode(data: bytes) -> list[str]:
+  """Encode bytes to a list of mnemonic words."""
+  words, _, _ = _load()
+  result = []
+  for n in range(words_required(len(data))):
+    grp = (n // 3) * 4
+    remaining = min(len(data) - grp, 4)
+    x = int.from_bytes(data[grp:grp + remaining], "little")
+    pos = n % 3
+    extra = 0
+    if pos == 2:
+      if remaining == 3:
+        extra = MN_BASE  # use one of the 7 short remainder words
+      x //= MN_BASE * MN_BASE
+    elif pos == 1:
+      x //= MN_BASE
+    result.append(words[x % MN_BASE + extra])
+  return result
+
+
+def decode(encoded: list[str]) -> bytes:
+  """Decode a list of mnemonic words back to bytes."""
+  _, word_index, _ = _load()
+
+  buf = bytearray(((len(encoded) // 3) + 1) * 4)
+  offset = 0
+
+  for word in encoded:
+    try:
+      idx = word_index[word.lower()]
+    except KeyError:
+      raise ValueError(f"unrecognized word: {word!r}") from None
+
+    grp = offset & ~3
+    x = int.from_bytes(buf[grp:grp + 4], "little")
+    pos = offset % 4
+
+    if pos == 3:
+      raise ValueError("unexpected word after 24-bit remainder")
+    elif pos == 0:
+      x = idx
+      offset += 1
+    elif pos == 1:
+      x += idx * MN_BASE
+      offset += 1
+    elif pos == 2:
+      if idx >= MN_BASE:  # 24-bit remainder word
+        x += (idx - MN_BASE) * MN_BASE * MN_BASE
+        offset += 1  # offset % 4 == 3 signals end-of-group next
+      else:
+        if idx > 1624 or (idx == 1624 and x > 1_312_671):
+          raise ValueError("invalid encoding: value out of range")
+        x += idx * MN_BASE * MN_BASE
+        offset += 2  # skip to next group
+
+    buf[grp:grp + 4] = x.to_bytes(4, "little")
+
+  return bytes(buf[:offset])
+
+
+if __name__ == "__main__":
+  import argparse
+
+  mode = "decode" if "decode" in sys.argv[0] else "encode"
+
+  parser = argparse.ArgumentParser(
+    prog=sys.argv[0],
+    description="Decode mnemonic words to bytes." if mode == "decode" else "Encode bytes to mnemonic words.",
+  )
+  parser.add_argument("-s", "--separator", default="-", help="word separator (default: -)")
+
+  args = parser.parse_args()
+
+  if mode == "encode":
+    data = sys.stdin.buffer.read()
+    print(args.separator.join(encode(data)))
+  else:
+    words = [w.strip() for w in sys.stdin.read().split(args.separator) if w.strip()]
+    try:
+      data = decode(words)
+    except ValueError as e:
+      print(f"error: {e}", file=sys.stderr)
+      sys.exit(1)
+    sys.stdout.buffer.write(data)

+ 73 - 0
rebuild/tools/parse_wordlist.py

@@ -0,0 +1,73 @@
+#!/usr/bin/env python3
+"""Parse mnemonicode mn_wordlist.c and emit the wordlist as JSON."""
+
+import argparse
+import json
+import re
+import sys
+from pathlib import Path
+
+
+def parse_wordlist(src: Path) -> dict:
+  text = src.read_text()
+
+  version_match = re.search(r'mn_wordlist_version\s*=\s*"[^"]*?([\d]+\.[\d]+)"', text)
+  if not version_match:
+    # Try alternate form: version embedded in longer string
+    version_match = re.search(r'mn_wordlist_version\s*=\s*"[^"]*?(\d[\d.]+\d)', text)
+  if not version_match:
+    raise ValueError("Could not find mn_wordlist_version in source file")
+  version = version_match.group(1)
+
+  array_match = re.search(r'mn_words\[.*?\]\s*=\s*\{(.*?)\};', text, re.DOTALL)
+  if not array_match:
+    raise ValueError("Could not find mn_words array in source file")
+  array_body = array_match.group(1)
+
+  # All quoted strings in the array body; first element is the null sentinel (0, not a string)
+  words = re.findall(r'"([^"]+)"', array_body)
+
+  return {"version": version, "words": words}
+
+
+def main():
+  parser = argparse.ArgumentParser(
+    description="Parse mnemonicode mn_wordlist.c and emit the wordlist as JSON"
+  )
+  parser.add_argument(
+    "source",
+    nargs="?",
+    default=str(Path(__file__).parent.parent / "mnemonicode/mn_wordlist.c"),
+    help="Path to mn_wordlist.c (default: ../mnemonicode/mn_wordlist.c relative to tools/)",
+  )
+  parser.add_argument(
+    "-o", "--output",
+    help="Write output to this file instead of stdout",
+  )
+  parser.add_argument(
+    "--indent",
+    type=int,
+    default=2,
+    help="JSON indent level (default: 2; use 0 for compact)",
+  )
+  args = parser.parse_args()
+
+  src = Path(args.source)
+  if not src.exists():
+    print(f"error: source file not found: {src}", file=sys.stderr)
+    sys.exit(1)
+
+  result = parse_wordlist(src)
+
+  indent = args.indent if args.indent > 0 else None
+  output = json.dumps(result, indent=indent, ensure_ascii=False)
+
+  if args.output:
+    Path(args.output).write_text(output + "\n")
+    print(f"wrote {len(result['words'])} words (version {result['version']}) -> {args.output}")
+  else:
+    print(output)
+
+
+if __name__ == "__main__":
+  main()

+ 1638 - 0
rebuild/wordlist.json

@@ -0,0 +1,1638 @@
+{
+  "version": "0.7",
+  "words": [
+    "academy",
+    "acrobat",
+    "active",
+    "actor",
+    "adam",
+    "admiral",
+    "adrian",
+    "africa",
+    "agenda",
+    "agent",
+    "airline",
+    "airport",
+    "aladdin",
+    "alarm",
+    "alaska",
+    "albert",
+    "albino",
+    "album",
+    "alcohol",
+    "alex",
+    "algebra",
+    "alibi",
+    "alice",
+    "alien",
+    "alpha",
+    "alpine",
+    "amadeus",
+    "amanda",
+    "amazon",
+    "amber",
+    "america",
+    "amigo",
+    "analog",
+    "anatomy",
+    "angel",
+    "animal",
+    "antenna",
+    "antonio",
+    "apollo",
+    "april",
+    "archive",
+    "arctic",
+    "arizona",
+    "arnold",
+    "aroma",
+    "arthur",
+    "artist",
+    "asia",
+    "aspect",
+    "aspirin",
+    "athena",
+    "athlete",
+    "atlas",
+    "audio",
+    "august",
+    "austria",
+    "axiom",
+    "aztec",
+    "balance",
+    "ballad",
+    "banana",
+    "bandit",
+    "banjo",
+    "barcode",
+    "baron",
+    "basic",
+    "battery",
+    "belgium",
+    "berlin",
+    "bermuda",
+    "bernard",
+    "bikini",
+    "binary",
+    "bingo",
+    "biology",
+    "block",
+    "blonde",
+    "bonus",
+    "boris",
+    "boston",
+    "boxer",
+    "brandy",
+    "bravo",
+    "brazil",
+    "bronze",
+    "brown",
+    "bruce",
+    "bruno",
+    "burger",
+    "burma",
+    "cabinet",
+    "cactus",
+    "cafe",
+    "cairo",
+    "cake",
+    "calypso",
+    "camel",
+    "camera",
+    "campus",
+    "canada",
+    "canal",
+    "cannon",
+    "canoe",
+    "cantina",
+    "canvas",
+    "canyon",
+    "capital",
+    "caramel",
+    "caravan",
+    "carbon",
+    "cargo",
+    "carlo",
+    "carol",
+    "carpet",
+    "cartel",
+    "casino",
+    "castle",
+    "castro",
+    "catalog",
+    "caviar",
+    "cecilia",
+    "cement",
+    "center",
+    "century",
+    "ceramic",
+    "chamber",
+    "chance",
+    "change",
+    "chaos",
+    "charlie",
+    "charm",
+    "charter",
+    "chef",
+    "chemist",
+    "cherry",
+    "chess",
+    "chicago",
+    "chicken",
+    "chief",
+    "china",
+    "cigar",
+    "cinema",
+    "circus",
+    "citizen",
+    "city",
+    "clara",
+    "classic",
+    "claudia",
+    "clean",
+    "client",
+    "climax",
+    "clinic",
+    "clock",
+    "club",
+    "cobra",
+    "coconut",
+    "cola",
+    "collect",
+    "colombo",
+    "colony",
+    "color",
+    "combat",
+    "comedy",
+    "comet",
+    "command",
+    "compact",
+    "company",
+    "complex",
+    "concept",
+    "concert",
+    "connect",
+    "consul",
+    "contact",
+    "context",
+    "contour",
+    "control",
+    "convert",
+    "copy",
+    "corner",
+    "corona",
+    "correct",
+    "cosmos",
+    "couple",
+    "courage",
+    "cowboy",
+    "craft",
+    "crash",
+    "credit",
+    "cricket",
+    "critic",
+    "crown",
+    "crystal",
+    "cuba",
+    "culture",
+    "dallas",
+    "dance",
+    "daniel",
+    "david",
+    "decade",
+    "decimal",
+    "deliver",
+    "delta",
+    "deluxe",
+    "demand",
+    "demo",
+    "denmark",
+    "derby",
+    "design",
+    "detect",
+    "develop",
+    "diagram",
+    "dialog",
+    "diamond",
+    "diana",
+    "diego",
+    "diesel",
+    "diet",
+    "digital",
+    "dilemma",
+    "diploma",
+    "direct",
+    "disco",
+    "disney",
+    "distant",
+    "doctor",
+    "dollar",
+    "dominic",
+    "domino",
+    "donald",
+    "dragon",
+    "drama",
+    "dublin",
+    "duet",
+    "dynamic",
+    "east",
+    "ecology",
+    "economy",
+    "edgar",
+    "egypt",
+    "elastic",
+    "elegant",
+    "element",
+    "elite",
+    "elvis",
+    "email",
+    "energy",
+    "engine",
+    "english",
+    "episode",
+    "equator",
+    "escort",
+    "ethnic",
+    "europe",
+    "everest",
+    "evident",
+    "exact",
+    "example",
+    "exit",
+    "exotic",
+    "export",
+    "express",
+    "extra",
+    "fabric",
+    "factor",
+    "falcon",
+    "family",
+    "fantasy",
+    "fashion",
+    "fiber",
+    "fiction",
+    "fidel",
+    "fiesta",
+    "figure",
+    "film",
+    "filter",
+    "final",
+    "finance",
+    "finish",
+    "finland",
+    "flash",
+    "florida",
+    "flower",
+    "fluid",
+    "flute",
+    "focus",
+    "ford",
+    "forest",
+    "formal",
+    "format",
+    "formula",
+    "fortune",
+    "forum",
+    "fragile",
+    "france",
+    "frank",
+    "friend",
+    "frozen",
+    "future",
+    "gabriel",
+    "galaxy",
+    "gallery",
+    "gamma",
+    "garage",
+    "garden",
+    "garlic",
+    "gemini",
+    "general",
+    "genetic",
+    "genius",
+    "germany",
+    "global",
+    "gloria",
+    "golf",
+    "gondola",
+    "gong",
+    "good",
+    "gordon",
+    "gorilla",
+    "grand",
+    "granite",
+    "graph",
+    "green",
+    "group",
+    "guide",
+    "guitar",
+    "guru",
+    "hand",
+    "happy",
+    "harbor",
+    "harmony",
+    "harvard",
+    "havana",
+    "hawaii",
+    "helena",
+    "hello",
+    "henry",
+    "hilton",
+    "history",
+    "horizon",
+    "hotel",
+    "human",
+    "humor",
+    "icon",
+    "idea",
+    "igloo",
+    "igor",
+    "image",
+    "impact",
+    "import",
+    "index",
+    "india",
+    "indigo",
+    "input",
+    "insect",
+    "instant",
+    "iris",
+    "italian",
+    "jacket",
+    "jacob",
+    "jaguar",
+    "janet",
+    "japan",
+    "jargon",
+    "jazz",
+    "jeep",
+    "john",
+    "joker",
+    "jordan",
+    "jumbo",
+    "june",
+    "jungle",
+    "junior",
+    "jupiter",
+    "karate",
+    "karma",
+    "kayak",
+    "kermit",
+    "kilo",
+    "king",
+    "koala",
+    "korea",
+    "labor",
+    "lady",
+    "lagoon",
+    "laptop",
+    "laser",
+    "latin",
+    "lava",
+    "lecture",
+    "left",
+    "legal",
+    "lemon",
+    "level",
+    "lexicon",
+    "liberal",
+    "libra",
+    "limbo",
+    "limit",
+    "linda",
+    "linear",
+    "lion",
+    "liquid",
+    "liter",
+    "little",
+    "llama",
+    "lobby",
+    "lobster",
+    "local",
+    "logic",
+    "logo",
+    "lola",
+    "london",
+    "lotus",
+    "lucas",
+    "lunar",
+    "machine",
+    "macro",
+    "madam",
+    "madonna",
+    "madrid",
+    "maestro",
+    "magic",
+    "magnet",
+    "magnum",
+    "major",
+    "mama",
+    "mambo",
+    "manager",
+    "mango",
+    "manila",
+    "marco",
+    "marina",
+    "market",
+    "mars",
+    "martin",
+    "marvin",
+    "master",
+    "matrix",
+    "maximum",
+    "media",
+    "medical",
+    "mega",
+    "melody",
+    "melon",
+    "memo",
+    "mental",
+    "mentor",
+    "menu",
+    "mercury",
+    "message",
+    "metal",
+    "meteor",
+    "meter",
+    "method",
+    "metro",
+    "mexico",
+    "miami",
+    "micro",
+    "million",
+    "mineral",
+    "minimum",
+    "minus",
+    "minute",
+    "miracle",
+    "mirage",
+    "miranda",
+    "mister",
+    "mixer",
+    "mobile",
+    "model",
+    "modem",
+    "modern",
+    "modular",
+    "moment",
+    "monaco",
+    "monica",
+    "monitor",
+    "mono",
+    "monster",
+    "montana",
+    "morgan",
+    "motel",
+    "motif",
+    "motor",
+    "mozart",
+    "multi",
+    "museum",
+    "music",
+    "mustang",
+    "natural",
+    "neon",
+    "nepal",
+    "neptune",
+    "nerve",
+    "neutral",
+    "nevada",
+    "news",
+    "ninja",
+    "nirvana",
+    "normal",
+    "nova",
+    "novel",
+    "nuclear",
+    "numeric",
+    "nylon",
+    "oasis",
+    "object",
+    "observe",
+    "ocean",
+    "octopus",
+    "olivia",
+    "olympic",
+    "omega",
+    "opera",
+    "optic",
+    "optimal",
+    "orange",
+    "orbit",
+    "organic",
+    "orient",
+    "origin",
+    "orlando",
+    "oscar",
+    "oxford",
+    "oxygen",
+    "ozone",
+    "pablo",
+    "pacific",
+    "pagoda",
+    "palace",
+    "pamela",
+    "panama",
+    "panda",
+    "panel",
+    "panic",
+    "paradox",
+    "pardon",
+    "paris",
+    "parker",
+    "parking",
+    "parody",
+    "partner",
+    "passage",
+    "passive",
+    "pasta",
+    "pastel",
+    "patent",
+    "patriot",
+    "patrol",
+    "patron",
+    "pegasus",
+    "pelican",
+    "penguin",
+    "pepper",
+    "percent",
+    "perfect",
+    "perfume",
+    "period",
+    "permit",
+    "person",
+    "peru",
+    "phone",
+    "photo",
+    "piano",
+    "picasso",
+    "picnic",
+    "picture",
+    "pigment",
+    "pilgrim",
+    "pilot",
+    "pirate",
+    "pixel",
+    "pizza",
+    "planet",
+    "plasma",
+    "plaster",
+    "plastic",
+    "plaza",
+    "pocket",
+    "poem",
+    "poetic",
+    "poker",
+    "polaris",
+    "police",
+    "politic",
+    "polo",
+    "polygon",
+    "pony",
+    "popcorn",
+    "popular",
+    "postage",
+    "postal",
+    "precise",
+    "prefix",
+    "premium",
+    "present",
+    "price",
+    "prince",
+    "printer",
+    "prism",
+    "private",
+    "product",
+    "profile",
+    "program",
+    "project",
+    "protect",
+    "proton",
+    "public",
+    "pulse",
+    "puma",
+    "pyramid",
+    "queen",
+    "radar",
+    "radio",
+    "random",
+    "rapid",
+    "rebel",
+    "record",
+    "recycle",
+    "reflex",
+    "reform",
+    "regard",
+    "regular",
+    "relax",
+    "report",
+    "reptile",
+    "reverse",
+    "ricardo",
+    "ringo",
+    "ritual",
+    "robert",
+    "robot",
+    "rocket",
+    "rodeo",
+    "romeo",
+    "royal",
+    "russian",
+    "safari",
+    "salad",
+    "salami",
+    "salmon",
+    "salon",
+    "salute",
+    "samba",
+    "sandra",
+    "santana",
+    "sardine",
+    "school",
+    "screen",
+    "script",
+    "second",
+    "secret",
+    "section",
+    "segment",
+    "select",
+    "seminar",
+    "senator",
+    "senior",
+    "sensor",
+    "serial",
+    "service",
+    "sheriff",
+    "shock",
+    "sierra",
+    "signal",
+    "silicon",
+    "silver",
+    "similar",
+    "simon",
+    "single",
+    "siren",
+    "slogan",
+    "social",
+    "soda",
+    "solar",
+    "solid",
+    "solo",
+    "sonic",
+    "soviet",
+    "special",
+    "speed",
+    "spiral",
+    "spirit",
+    "sport",
+    "static",
+    "station",
+    "status",
+    "stereo",
+    "stone",
+    "stop",
+    "street",
+    "strong",
+    "student",
+    "studio",
+    "style",
+    "subject",
+    "sultan",
+    "super",
+    "susan",
+    "sushi",
+    "suzuki",
+    "switch",
+    "symbol",
+    "system",
+    "tactic",
+    "tahiti",
+    "talent",
+    "tango",
+    "tarzan",
+    "taxi",
+    "telex",
+    "tempo",
+    "tennis",
+    "texas",
+    "textile",
+    "theory",
+    "thermos",
+    "tiger",
+    "titanic",
+    "tokyo",
+    "tomato",
+    "topic",
+    "tornado",
+    "toronto",
+    "torpedo",
+    "total",
+    "totem",
+    "tourist",
+    "tractor",
+    "traffic",
+    "transit",
+    "trapeze",
+    "travel",
+    "tribal",
+    "trick",
+    "trident",
+    "trilogy",
+    "tripod",
+    "tropic",
+    "trumpet",
+    "tulip",
+    "tuna",
+    "turbo",
+    "twist",
+    "ultra",
+    "uniform",
+    "union",
+    "uranium",
+    "vacuum",
+    "valid",
+    "vampire",
+    "vanilla",
+    "vatican",
+    "velvet",
+    "ventura",
+    "venus",
+    "vertigo",
+    "veteran",
+    "victor",
+    "video",
+    "vienna",
+    "viking",
+    "village",
+    "vincent",
+    "violet",
+    "violin",
+    "virtual",
+    "virus",
+    "visa",
+    "vision",
+    "visitor",
+    "visual",
+    "vitamin",
+    "viva",
+    "vocal",
+    "vodka",
+    "volcano",
+    "voltage",
+    "volume",
+    "voyage",
+    "water",
+    "weekend",
+    "welcome",
+    "western",
+    "window",
+    "winter",
+    "wizard",
+    "wolf",
+    "world",
+    "xray",
+    "yankee",
+    "yoga",
+    "yogurt",
+    "yoyo",
+    "zebra",
+    "zero",
+    "zigzag",
+    "zipper",
+    "zodiac",
+    "zoom",
+    "abraham",
+    "action",
+    "address",
+    "alabama",
+    "alfred",
+    "almond",
+    "ammonia",
+    "analyze",
+    "annual",
+    "answer",
+    "apple",
+    "arena",
+    "armada",
+    "arsenal",
+    "atlanta",
+    "atomic",
+    "avenue",
+    "average",
+    "bagel",
+    "baker",
+    "ballet",
+    "bambino",
+    "bamboo",
+    "barbara",
+    "basket",
+    "bazaar",
+    "benefit",
+    "bicycle",
+    "bishop",
+    "blitz",
+    "bonjour",
+    "bottle",
+    "bridge",
+    "british",
+    "brother",
+    "brush",
+    "budget",
+    "cabaret",
+    "cadet",
+    "candle",
+    "capitan",
+    "capsule",
+    "career",
+    "cartoon",
+    "channel",
+    "chapter",
+    "cheese",
+    "circle",
+    "cobalt",
+    "cockpit",
+    "college",
+    "compass",
+    "comrade",
+    "condor",
+    "crimson",
+    "cyclone",
+    "darwin",
+    "declare",
+    "degree",
+    "delete",
+    "delphi",
+    "denver",
+    "desert",
+    "divide",
+    "dolby",
+    "domain",
+    "domingo",
+    "double",
+    "drink",
+    "driver",
+    "eagle",
+    "earth",
+    "echo",
+    "eclipse",
+    "editor",
+    "educate",
+    "edward",
+    "effect",
+    "electra",
+    "emerald",
+    "emotion",
+    "empire",
+    "empty",
+    "escape",
+    "eternal",
+    "evening",
+    "exhibit",
+    "expand",
+    "explore",
+    "extreme",
+    "ferrari",
+    "first",
+    "flag",
+    "folio",
+    "forget",
+    "forward",
+    "freedom",
+    "fresh",
+    "friday",
+    "fuji",
+    "galileo",
+    "garcia",
+    "genesis",
+    "gold",
+    "gravity",
+    "habitat",
+    "hamlet",
+    "harlem",
+    "helium",
+    "holiday",
+    "house",
+    "hunter",
+    "ibiza",
+    "iceberg",
+    "imagine",
+    "infant",
+    "isotope",
+    "jackson",
+    "jamaica",
+    "jasmine",
+    "java",
+    "jessica",
+    "judo",
+    "kitchen",
+    "lazarus",
+    "letter",
+    "license",
+    "lithium",
+    "loyal",
+    "lucky",
+    "magenta",
+    "mailbox",
+    "manual",
+    "marble",
+    "mary",
+    "maxwell",
+    "mayor",
+    "milk",
+    "monarch",
+    "monday",
+    "money",
+    "morning",
+    "mother",
+    "mystery",
+    "native",
+    "nectar",
+    "nelson",
+    "network",
+    "next",
+    "nikita",
+    "nobel",
+    "nobody",
+    "nominal",
+    "norway",
+    "nothing",
+    "number",
+    "october",
+    "office",
+    "oliver",
+    "opinion",
+    "option",
+    "order",
+    "outside",
+    "package",
+    "pancake",
+    "pandora",
+    "panther",
+    "papa",
+    "patient",
+    "pattern",
+    "pedro",
+    "pencil",
+    "people",
+    "phantom",
+    "philips",
+    "pioneer",
+    "pluto",
+    "podium",
+    "portal",
+    "potato",
+    "prize",
+    "process",
+    "protein",
+    "proxy",
+    "pump",
+    "pupil",
+    "python",
+    "quality",
+    "quarter",
+    "quiet",
+    "rabbit",
+    "radical",
+    "radius",
+    "rainbow",
+    "ralph",
+    "ramirez",
+    "ravioli",
+    "raymond",
+    "respect",
+    "respond",
+    "result",
+    "resume",
+    "retro",
+    "richard",
+    "right",
+    "risk",
+    "river",
+    "roger",
+    "roman",
+    "rondo",
+    "sabrina",
+    "salary",
+    "salsa",
+    "sample",
+    "samuel",
+    "saturn",
+    "savage",
+    "scarlet",
+    "scoop",
+    "scorpio",
+    "scratch",
+    "scroll",
+    "sector",
+    "serpent",
+    "shadow",
+    "shampoo",
+    "sharon",
+    "sharp",
+    "short",
+    "shrink",
+    "silence",
+    "silk",
+    "simple",
+    "slang",
+    "smart",
+    "smoke",
+    "snake",
+    "society",
+    "sonar",
+    "sonata",
+    "soprano",
+    "source",
+    "sparta",
+    "sphere",
+    "spider",
+    "sponsor",
+    "spring",
+    "acid",
+    "adios",
+    "agatha",
+    "alamo",
+    "alert",
+    "almanac",
+    "aloha",
+    "andrea",
+    "anita",
+    "arcade",
+    "aurora",
+    "avalon",
+    "baby",
+    "baggage",
+    "balloon",
+    "bank",
+    "basil",
+    "begin",
+    "biscuit",
+    "blue",
+    "bombay",
+    "brain",
+    "brenda",
+    "brigade",
+    "cable",
+    "carmen",
+    "cello",
+    "celtic",
+    "chariot",
+    "chrome",
+    "citrus",
+    "civil",
+    "cloud",
+    "common",
+    "compare",
+    "cool",
+    "copper",
+    "coral",
+    "crater",
+    "cubic",
+    "cupid",
+    "cycle",
+    "depend",
+    "door",
+    "dream",
+    "dynasty",
+    "edison",
+    "edition",
+    "enigma",
+    "equal",
+    "eric",
+    "event",
+    "evita",
+    "exodus",
+    "extend",
+    "famous",
+    "farmer",
+    "food",
+    "fossil",
+    "frog",
+    "fruit",
+    "geneva",
+    "gentle",
+    "george",
+    "giant",
+    "gilbert",
+    "gossip",
+    "gram",
+    "greek",
+    "grille",
+    "hammer",
+    "harvest",
+    "hazard",
+    "heaven",
+    "herbert",
+    "heroic",
+    "hexagon",
+    "husband",
+    "immune",
+    "inca",
+    "inch",
+    "initial",
+    "isabel",
+    "ivory",
+    "jason",
+    "jerome",
+    "joel",
+    "joshua",
+    "journal",
+    "judge",
+    "juliet",
+    "jump",
+    "justice",
+    "kimono",
+    "kinetic",
+    "leonid",
+    "lima",
+    "maze",
+    "medusa",
+    "member",
+    "memphis",
+    "michael",
+    "miguel",
+    "milan",
+    "mile",
+    "miller",
+    "mimic",
+    "mimosa",
+    "mission",
+    "monkey",
+    "moral",
+    "moses",
+    "mouse",
+    "nancy",
+    "natasha",
+    "nebula",
+    "nickel",
+    "nina",
+    "noise",
+    "orchid",
+    "oregano",
+    "origami",
+    "orinoco",
+    "orion",
+    "othello",
+    "paper",
+    "paprika",
+    "prelude",
+    "prepare",
+    "pretend",
+    "profit",
+    "promise",
+    "provide",
+    "puzzle",
+    "remote",
+    "repair",
+    "reply",
+    "rival",
+    "riviera",
+    "robin",
+    "rose",
+    "rover",
+    "rudolf",
+    "saga",
+    "sahara",
+    "scholar",
+    "shelter",
+    "ship",
+    "shoe",
+    "sigma",
+    "sister",
+    "sleep",
+    "smile",
+    "spain",
+    "spark",
+    "split",
+    "spray",
+    "square",
+    "stadium",
+    "star",
+    "storm",
+    "story",
+    "strange",
+    "stretch",
+    "stuart",
+    "subway",
+    "sugar",
+    "sulfur",
+    "summer",
+    "survive",
+    "sweet",
+    "swim",
+    "table",
+    "taboo",
+    "target",
+    "teacher",
+    "telecom",
+    "temple",
+    "tibet",
+    "ticket",
+    "tina",
+    "today",
+    "toga",
+    "tommy",
+    "tower",
+    "trivial",
+    "tunnel",
+    "turtle",
+    "twin",
+    "uncle",
+    "unicorn",
+    "unique",
+    "update",
+    "valery",
+    "vega",
+    "version",
+    "voodoo",
+    "warning",
+    "william",
+    "wonder",
+    "year",
+    "yellow",
+    "young",
+    "absent",
+    "absorb",
+    "accent",
+    "alfonso",
+    "alias",
+    "ambient",
+    "andy",
+    "anvil",
+    "appear",
+    "apropos",
+    "archer",
+    "ariel",
+    "armor",
+    "arrow",
+    "austin",
+    "avatar",
+    "axis",
+    "baboon",
+    "bahama",
+    "bali",
+    "balsa",
+    "bazooka",
+    "beach",
+    "beast",
+    "beatles",
+    "beauty",
+    "before",
+    "benny",
+    "betty",
+    "between",
+    "beyond",
+    "billy",
+    "bison",
+    "blast",
+    "bless",
+    "bogart",
+    "bonanza",
+    "book",
+    "border",
+    "brave",
+    "bread",
+    "break",
+    "broken",
+    "bucket",
+    "buenos",
+    "buffalo",
+    "bundle",
+    "button",
+    "buzzer",
+    "byte",
+    "caesar",
+    "camilla",
+    "canary",
+    "candid",
+    "carrot",
+    "cave",
+    "chant",
+    "child",
+    "choice",
+    "chris",
+    "cipher",
+    "clarion",
+    "clark",
+    "clever",
+    "cliff",
+    "clone",
+    "conan",
+    "conduct",
+    "congo",
+    "content",
+    "costume",
+    "cotton",
+    "cover",
+    "crack",
+    "current",
+    "danube",
+    "data",
+    "decide",
+    "desire",
+    "detail",
+    "dexter",
+    "dinner",
+    "dispute",
+    "donor",
+    "druid",
+    "drum",
+    "easy",
+    "eddie",
+    "enjoy",
+    "enrico",
+    "epoxy",
+    "erosion",
+    "except",
+    "exile",
+    "explain",
+    "fame",
+    "fast",
+    "father",
+    "felix",
+    "field",
+    "fiona",
+    "fire",
+    "fish",
+    "flame",
+    "flex",
+    "flipper",
+    "float",
+    "flood",
+    "floor",
+    "forbid",
+    "forever",
+    "fractal",
+    "frame",
+    "freddie",
+    "front",
+    "fuel",
+    "gallop",
+    "game",
+    "garbo",
+    "gate",
+    "gibson",
+    "ginger",
+    "giraffe",
+    "gizmo",
+    "glass",
+    "goblin",
+    "gopher",
+    "grace",
+    "gray",
+    "gregory",
+    "grid",
+    "griffin",
+    "ground",
+    "guest",
+    "gustav",
+    "gyro",
+    "hair",
+    "halt",
+    "harris",
+    "heart",
+    "heavy",
+    "herman",
+    "hippie",
+    "hobby",
+    "honey",
+    "hope",
+    "horse",
+    "hostel",
+    "hydro",
+    "imitate",
+    "info",
+    "ingrid",
+    "inside",
+    "invent",
+    "invest",
+    "invite",
+    "iron",
+    "ivan",
+    "james",
+    "jester",
+    "jimmy",
+    "join",
+    "joseph",
+    "juice",
+    "julius",
+    "july",
+    "justin",
+    "kansas",
+    "karl",
+    "kevin",
+    "kiwi",
+    "ladder",
+    "lake",
+    "laura",
+    "learn",
+    "legacy",
+    "legend",
+    "lesson",
+    "life",
+    "light",
+    "list",
+    "locate",
+    "lopez",
+    "lorenzo",
+    "love",
+    "lunch",
+    "malta",
+    "mammal",
+    "margo",
+    "marion",
+    "mask",
+    "match",
+    "mayday",
+    "meaning",
+    "mercy",
+    "middle",
+    "mike",
+    "mirror",
+    "modest",
+    "morph",
+    "morris",
+    "nadia",
+    "nato",
+    "navy",
+    "needle",
+    "neuron",
+    "never",
+    "newton",
+    "nice",
+    "night",
+    "nissan",
+    "nitro",
+    "nixon",
+    "north",
+    "oberon",
+    "octavia",
+    "ohio",
+    "olga",
+    "open",
+    "opus",
+    "orca",
+    "oval",
+    "owner",
+    "page",
+    "paint",
+    "palma",
+    "parade",
+    "parent",
+    "parole",
+    "paul",
+    "peace",
+    "pearl",
+    "perform",
+    "phoenix",
+    "phrase",
+    "pierre",
+    "pinball",
+    "place",
+    "plate",
+    "plato",
+    "plume",
+    "pogo",
+    "point",
+    "polite",
+    "polka",
+    "poncho",
+    "powder",
+    "prague",
+    "press",
+    "presto",
+    "pretty",
+    "prime",
+    "promo",
+    "quasi",
+    "quest",
+    "quick",
+    "quiz",
+    "quota",
+    "race",
+    "rachel",
+    "raja",
+    "ranger",
+    "region",
+    "remark",
+    "rent",
+    "reward",
+    "rhino",
+    "ribbon",
+    "rider",
+    "road",
+    "rodent",
+    "round",
+    "rubber",
+    "ruby",
+    "rufus",
+    "sabine",
+    "saddle",
+    "sailor",
+    "saint",
+    "salt",
+    "satire",
+    "scale",
+    "scuba",
+    "season",
+    "secure",
+    "shake",
+    "shallow",
+    "shannon",
+    "shave",
+    "shelf",
+    "sherman",
+    "shine",
+    "shirt",
+    "side",
+    "sinatra",
+    "sincere",
+    "size",
+    "slalom",
+    "slow",
+    "small",
+    "snow",
+    "sofia",
+    "song",
+    "sound",
+    "south",
+    "speech",
+    "spell",
+    "spend",
+    "spoon",
+    "stage",
+    "stamp",
+    "stand",
+    "state",
+    "stella",
+    "stick",
+    "sting",
+    "stock",
+    "store",
+    "sunday",
+    "sunset",
+    "support",
+    "sweden",
+    "swing",
+    "tape",
+    "think",
+    "thomas",
+    "tictac",
+    "time",
+    "toast",
+    "tobacco",
+    "tonight",
+    "torch",
+    "torso",
+    "touch",
+    "toyota",
+    "trade",
+    "tribune",
+    "trinity",
+    "triton",
+    "truck",
+    "trust",
+    "type",
+    "under",
+    "unit",
+    "urban",
+    "urgent",
+    "user",
+    "value",
+    "vendor",
+    "venice",
+    "verona",
+    "vibrate",
+    "virgo",
+    "visible",
+    "vista",
+    "vital",
+    "voice",
+    "vortex",
+    "waiter",
+    "watch",
+    "wave",
+    "weather",
+    "wedding",
+    "wheel",
+    "whiskey",
+    "wisdom",
+    "deal",
+    "null",
+    "nurse",
+    "quebec",
+    "reserve",
+    "reunion",
+    "roof",
+    "singer",
+    "verbal",
+    "amen",
+    "ego",
+    "fax",
+    "jet",
+    "job",
+    "rio",
+    "ski",
+    "yes"
+  ]
+}