]> Untitled Git - axy/ft/rag.git/commitdiff
Improved type checking and docstrings
authorAxy <gilliardmarthey.axel@gmail.com>
Fri, 28 Aug 2026 13:55:36 +0000 (15:55 +0200)
committerAxy <gilliardmarthey.axel@gmail.com>
Fri, 28 Aug 2026 13:55:36 +0000 (15:55 +0200)
12 files changed:
.gitignore
Makefile [new file with mode: 0644]
pyproject.toml
src/__main__.py
src/rag/__init__.py
src/rag/_stoopid.py
src/rag/answering.py
src/rag/chunking.py
src/rag/evaluate.py
src/rag/models.py
src/rag/retrieval.py
uv.lock

index f8555d9337a7896fad5d7981753b8029f52d7f68..583c3529d54bbe2d59f907224adb4fdbcfe13a0c 100644 (file)
@@ -2,3 +2,5 @@
 vllm-0.10.1
 data
 moulinette*
+.mypy_cache
+.ruff_cache
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..9757c71
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,23 @@
+install:
+       uv sync
+
+run:
+       uv run rag --help
+
+debug:
+       uv run -m -- pdb -m src
+
+clean:
+       find src -type d \( -name "__pycache__" -or -name ".mypy_cache" -or -name ".ruff_cache" \) -exec rm -r {} +
+
+lint:
+       uv run flake8 src
+       uv run mypy src  --warn-return-any --warn-unused-ignores --ignore-missing-imports --disallow-untyped-defs --check-untyped-defs
+
+lint-strict:
+       uv run flake8 src
+       uv run mypy src --strict
+
+check:
+       uv run ty check
+       uv run ruff check
index b712de9a4be691a082ea89c301faaa9afb966c1f..801a9a76d2699fb29d096f9106277f0f7889ecbc 100644 (file)
@@ -20,6 +20,7 @@ dev = [
     "flake8>=7.3.0",
     "mypy>=2.3.1",
     "ruff>=0.16.4",
+    "ty>=0.0.75",
     "types-tqdm>=4.70.0.20260805",
 ]
 
@@ -39,7 +40,10 @@ url = "https://download.pytorch.org/whl/cpu"
 
 [tool.ruff]
 line-length = 79
-lint.select = ["E", "F", "UP", "B", "SIM", "I", "ARG", "N"]
+lint.select = ["E", "F", "UP", "B", "SIM", "I", "ARG", "N", "D"]
+
+[tool.ruff.lint.pydocstyle]
+convention = "google"
 
 [tool.uv]
 preview-features = ["format", "check"]
@@ -47,3 +51,7 @@ preview-features = ["format", "check"]
 [[tool.mypy.overrides]]
 module = ["fire"]
 follow_untyped_imports = true
+
+[tool.ty.rules]
+all = "error"
+possibly-missing-import = "ignore"
index 4efdf78f30ec80a27601b79a772f14c42ae5de5e..64d4e70e956ff393e6fa98c11148ce0582b1340e 100644 (file)
@@ -1,3 +1,5 @@
+"""A retrieval augmented generation CLI."""
+
 from rag import main
 
 if __name__ == "__main__":
index 38ddae5bc08d56d3517fbd00645945d5e8901cea..76919b7c39f6c56dea5bce8869268edf9c67489f 100644 (file)
@@ -1,3 +1,5 @@
+"""A retrieval augmented generation CLI."""
+
 import logging
 import os
 import traceback
@@ -13,7 +15,7 @@ from rag.chunking import FileType, chunk_file
 from rag.evaluate import sources_overlap
 from rag.models import (
     AnswerSet,
-    RagDataset,
+    RagQuestions,
     RetrievedQuestion,
     SearchAnswers,
     SearchResults,
@@ -23,6 +25,7 @@ from rag.retrieval import BM25, BagOfWords, stopwords
 
 
 def main() -> None:
+    """Run RAG using python fire, handling all exceptions."""
     try:
         fire.Fire(RAG())
     except Exception as e:
@@ -38,9 +41,10 @@ key_adapter: pydantic.TypeAdapter[Source] = pydantic.TypeAdapter(Source)
 
 
 class RAG:
-    """A retrieval augmented generation CLI"""
+    """A retrieval augmented generation CLI."""
 
     def __init__(self) -> None:
+        """Set up the caching state."""
         self._bm25_store: BM25[Source] | None = None
         self._inference_store: InferenceBackend | None = None
 
@@ -78,6 +82,7 @@ class RAG:
         index: str = "data/processed",
         max_chunk_size: int = 2000,
     ) -> None:
+        """Index a directory and its contents for later querying."""
         if max_chunk_size < 1:
             logging.getLogger(__name__).error(
                 f"Invalid max chunk size {max_chunk_size}"
@@ -113,7 +118,7 @@ class RAG:
             return self._bm25_store
         bm25: BM25[Source] = self._readf(
             index + "/index.json",
-            lambda s: BM25.from_storage(
+            lambda s: BM25[Source].from_storage(
                 (key_adapter.validate_json(k), v)
                 for k, v in tqdm(
                     storage_adapter.validate_json(s).items(),
@@ -132,6 +137,7 @@ class RAG:
     def search(
         self, query: str, /, k: int = 5, index: str = "data/processed"
     ) -> None:
+        """Retrieve sources for a querie."""
         if k < 1:
             logging.getLogger(__name__).error(f"Invalid k {k}")
             exit(1)
@@ -146,11 +152,12 @@ class RAG:
         k: int = 5,
         index: str = "data/processed",
     ) -> None:
+        """Retrieve sources for queries."""
         if k < 1:
             logging.getLogger(__name__).error(f"Invalid k {k}")
             exit(1)
         bm25 = self._bm25(index)
-        dataset = self._readf(dataset_path, RagDataset.model_validate_json)
+        dataset = self._readf(dataset_path, RagQuestions.model_validate_json)
         result = SearchResults(
             search_results=[
                 RetrievedQuestion(
@@ -183,6 +190,7 @@ class RAG:
         index: str = "data/processed",
         max_tokens: int = 100,
     ) -> None:
+        """Answer a query, retrieving sources and using an LLM."""
         if k < 1:
             logging.getLogger(__name__).error(f"Invalid k {k}")
             exit(1)
@@ -210,6 +218,7 @@ class RAG:
         /,
         max_tokens: int = 100,
     ) -> None:
+        """Answer queries given search results, using an LLM."""
         if max_tokens < 1:
             logging.getLogger(__name__).error(
                 f"Invalid max tokens {max_tokens}"
@@ -267,6 +276,7 @@ class RAG:
         k: int = 5,
         iou: float = 0.05,
     ) -> None:
+        """Evaluate the student search results for overlap with dataset."""
         if k < 1:
             logging.getLogger(__name__).error(f"Invalid k {k}")
             exit(1)
index 991966e8e07ecc5134bd72868dff009b675513f6..a0d732f6fde33b47391030de629dd24c5306b53a 100644 (file)
@@ -1,5 +1,4 @@
-"""Useless module where I can shove the required but overcomplicated and
-poorly designed pydantic classes."""
+"""Useless module where I can shove the required poorly pydantic models."""
 
 import uuid
 
index 008d13dd47ec3e46c701d064c54f663741e2dc67..acc5898f2ebeafb3d93045b485ea0dd283481a48 100644 (file)
@@ -1,11 +1,15 @@
+"""Answering questions using LLMs."""
+
 import logging
 from collections.abc import Callable
-from typing import Any, cast
+from typing import Any, cast, override
 
 from rag.models import AnsweredQuestion, RetrievedQuestion, Source
 
 
 class InferenceBackend:
+    """A simple inference backend using transformers."""
+
     SYSTEM_PROMPT: str = """You are a codebase and document search assistant.
     You answer querries factually using the provided sources.
     Answer directly with no reasoning, no <think> tags.
@@ -15,12 +19,13 @@ class InferenceBackend:
     End your answer with a newline."""
 
     def __init__(self, model: str) -> None:
+        """Initialize the backend fetching the given model from huggingface."""
         from transformers import AutoModelForCausalLM, AutoTokenizer
 
         self.model: Any = AutoModelForCausalLM.from_pretrained(
             model, device_map="auto"
         )
-        self.tokenizer = AutoTokenizer.from_pretrained(
+        self.tokenizer: Any = AutoTokenizer.from_pretrained(
             model, padding_side="left"
         )
 
@@ -64,7 +69,7 @@ class InferenceBackend:
         max_tokens: int,
         cb: Callable[[str], None],
     ) -> str:
-        from transformers.generation import BaseStreamer  # type: ignore
+        from transformers.generation.streamers import BaseStreamer
 
         class Streamer(BaseStreamer):
             def __init__(
@@ -74,6 +79,7 @@ class InferenceBackend:
                 self.cb = cb
                 self.skip = True
 
+            @override
             def put(self, value: Any) -> None:
                 if self.skip:
                     self.skip = False
@@ -86,6 +92,7 @@ class InferenceBackend:
                 )
                 self.cb(s.rstrip("\n"))
 
+            @override
             def end(self) -> None:
                 self.cb("\n")
 
@@ -100,10 +107,13 @@ class InferenceBackend:
             tokenizer=self.tokenizer,
             streamer=Streamer(self, cb),
         )
-        full_response = self.tokenizer.batch_decode(
-            generated_ids,
-            skip_special_tokens=True,
-        )[0]
+        full_response: str = cast(
+            str,
+            self.tokenizer.batch_decode(
+                generated_ids,
+                skip_special_tokens=True,
+            )[0],
+        )
         return full_response[len(prompt) :].strip()
 
     def answer(
@@ -112,6 +122,7 @@ class InferenceBackend:
         max_tokens: int,
         cb: Callable[[str], None] = lambda _s: None,
     ) -> AnsweredQuestion:
+        """Answers the given question."""
         return AnsweredQuestion(
             question_id=question.question_id,
             question=question.question,
index e14c6c35828709cf7218bde9b8e0bb6d58151b00..671aee1c7e29b1e2198cc25b68a2fc1a239ae9e7 100644 (file)
@@ -1,3 +1,5 @@
+"""Utilities for chunking a file into chunks respecting filetype semantics."""
+
 import logging
 from enum import Enum, auto
 from itertools import count
@@ -6,10 +8,12 @@ from rag.models import Source
 
 
 class FileType(Enum):
+    """A supported file type enum."""
+
     PYTHON = auto()
     TEXT = auto()
 
-    def line_depth(self, line: str) -> int:
+    def _line_depth(self, line: str) -> int:
         match self:
             case FileType.PYTHON:
                 return next(
@@ -30,12 +34,12 @@ class FileType(Enum):
                 )
 
 
-def chunk_by_depth(
+def _chunk_by_depth(
     s: str, max_chunk_size: int, by: FileType, layer: int = 0
 ) -> list[str]:
     chunks = [""]
     for line in s.splitlines(True):
-        depth = by.line_depth(line)
+        depth = by._line_depth(line)
         if depth is None or depth > layer:
             chunks[-1] += line
         else:
@@ -52,11 +56,11 @@ def chunk_by_depth(
                 s[i : min(len(chunks), i + max_chunk_size)]
                 for i in range(0, len(chunks), max_chunk_size)
             ]
-    res = []
+    res: list[str] = []
     can_extend = False
     for chunk in chunks:
         if len(chunk) > max_chunk_size:
-            res.extend(chunk_by_depth(chunk, max_chunk_size, by, layer + 1))
+            res.extend(_chunk_by_depth(chunk, max_chunk_size, by, layer + 1))
             can_extend = False
         elif not can_extend or len(chunk) + len(res[-1]) > max_chunk_size:
             res.append(chunk)
@@ -69,6 +73,7 @@ def chunk_by_depth(
 def chunk_file(
     path: str, max_chunk_size: int, by: FileType
 ) -> dict[Source, str]:
+    """Appropriately chunk a file of a given filetype into smaller chunks."""
     try:
         with open(path) as f:
             s = f.read()
@@ -77,7 +82,7 @@ def chunk_file(
             f"Error while chunking file {path}, skipping ({e})"
         )
         return {}
-    chunks = chunk_by_depth(s, max_chunk_size, by)
+    chunks = _chunk_by_depth(s, max_chunk_size, by)
     total_len = 0
     res = {}
     for chunk in chunks:
index 090ff5afc5b980bd1b5586d60b671c00031c703e..296743f3bfd6250b2cbaa40feecc4f7c0e74caf8 100644 (file)
@@ -1,7 +1,10 @@
+"""Utilities for evaluating search results compared to expected results."""
+
 from rag.models import Source
 
 
 def sources_overlap(a: Source, b: Source, min_iou: float) -> bool:
+    """Check whether two sources overlap with a minimum IoU."""
     union_start = min(a.first_character_index, b.first_character_index)
     union_end = max(a.last_character_index, b.last_character_index) + 1
     union = union_end - union_start
index 4ba9f7854034c734448d06be9f162c7a86c9d81c..7508598492f18ecb62c9b7996dd7c48e71e7892a 100644 (file)
@@ -1,14 +1,21 @@
+"""Pydantic models representing the inputs and outputs of the program."""
+
 import uuid
+from typing import override
 
 from pydantic import BaseModel, Field
 
 
 class Source(BaseModel, frozen=True):
+    """A source identifying a file and span within in."""
+
     file_path: str
     first_character_index: int
     last_character_index: int
 
+    @override
     def __str__(self) -> str:
+        """Represent the source in the suggested format."""
         return (
             f"{self.file_path}"
             + f" [{self.first_character_index}:"
@@ -17,33 +24,47 @@ class Source(BaseModel, frozen=True):
 
 
 class Question(BaseModel):
+    """A queried question, without further processing."""
+
     question_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
     question: str
 
 
-class RagDataset(BaseModel):
-    rag_questions: list[Question]
-
-
 class RetrievedQuestion(Question):
+    """A queried question, with search results."""
+
     retrieved_sources: list[Source]
 
 
 class AnsweredQuestion(RetrievedQuestion):
+    """A queried question, with search results and LLM answers."""
+
     answer: str
 
 
+class RagQuestions(BaseModel):
+    """A set of queried question, without further processing."""
+
+    rag_questions: list[Question]
+
+
 class SearchResults(BaseModel):
+    """A set of queried questions, with search results."""
+
     search_results: list[RetrievedQuestion]
     k: int
 
 
 class SearchAnswers(BaseModel):
+    """A set of queried questions, with search results and LLM answers."""
+
     search_results: list[AnsweredQuestion]
     k: int
 
 
 class DatasetAnswer(BaseModel):
+    """An answer according to the dataset schema."""
+
     question_id: str
     question: str
     answer: str
@@ -53,4 +74,6 @@ class DatasetAnswer(BaseModel):
 
 
 class AnswerSet(BaseModel):
+    """A set of answers according to the dataset schema."""
+
     rag_questions: list[DatasetAnswer]
index e62fb750da0ae7f75e5d380f4dc186621104ad43..fcc6051d823549e70f15c72947cf53d112b5b298 100644 (file)
@@ -1,3 +1,5 @@
+"""Retrieval indexing and lookup for a corpus of text."""
+
 import math
 from collections.abc import Iterable
 from dataclasses import dataclass
@@ -10,6 +12,7 @@ type BagOfWords = Multiset[str]
 
 
 def words_normalize(s: str) -> Iterable[str]:
+    """Split a string into normalized words."""
     start = 0
     for i, c in enumerate(s):
         if c.isalnum():
@@ -22,6 +25,7 @@ def words_normalize(s: str) -> Iterable[str]:
 
 
 def bag_of_words(s: str, stopwords: set[str]) -> BagOfWords:
+    """Split a string into a normalized bag of words, ignoring stopwords."""
     res: BagOfWords = {}
     for word in words_normalize(s):
         if word in stopwords:
@@ -31,6 +35,7 @@ def bag_of_words(s: str, stopwords: set[str]) -> BagOfWords:
 
 
 def stopwords(lang: str | Iterable[str]) -> set[str]:
+    """Return a usable set of normalized stopwords in lang."""
     return {
         word
         for e in stopwordsiso.stopwords(lang)
@@ -40,6 +45,8 @@ def stopwords(lang: str | Iterable[str]) -> set[str]:
 
 @dataclass
 class BM25[T]:
+    """A BM25 index for a given corpus allowing for fast lookups."""
+
     corpus_words: int
     corpus: dict[T, tuple[int, BagOfWords]]
     word_usage: dict[str, set[T]]
@@ -48,6 +55,7 @@ class BM25[T]:
 
     @staticmethod
     def from_storage(storage: Iterable[tuple[T, BagOfWords]]) -> "BM25[T]":
+        """Load an index from its simplifed representation."""
         corpus = {}
         word_usage: dict[str, set[T]] = {}
         corpus_words = 0
@@ -62,43 +70,48 @@ class BM25[T]:
         return BM25(corpus_words, corpus, word_usage)
 
     def to_storage(self) -> Iterable[tuple[T, BagOfWords]]:
+        """Transform an index to its simplified representation."""
         return ((k, v[1]) for k, v in self.corpus.items())
 
     @staticmethod
     def from_corpus(
         raw_corpus: Iterable[tuple[T, str]], stopwords: set[str] | None = None
     ) -> "BM25[T]":
-        return BM25.from_storage(
+        """Create an index for a given corpus of texts, ignoring stopwrods."""
+        return BM25[T].from_storage(
             (k, bow)
             for k, s in raw_corpus
             if (bow := bag_of_words(s, stopwords if stopwords else set()))
         )
 
-    def idf(self, q: str) -> float:
+    def _idf(self, q: str) -> float:
         n_q = len(self.word_usage.get(q, set()))
         n = len(self.corpus)
         quotient = (n - n_q + 0.5) / (n_q + 0.5)
         return math.log(quotient + 1)
 
     def word_score(self, word: str, ident: T) -> float:
+        """Return the score of a document for a single query word."""
         doc_words, doc = self.corpus[ident]
         f_q = doc.get(word, 0)
         freq = f_q * (self.k + 1)
         avgdl = self.corpus_words / len(self.corpus)
         freq_bias = 1 - self.b + self.b * (doc_words / avgdl)
         quotient = freq / (f_q + self.k * freq_bias)
-        return self.idf(word) * quotient
+        return self._idf(word) * quotient
 
-    def score(self, querry: str, doc: T) -> float:
-        return sum(self.word_score(q, doc) for q in words_normalize(querry))
+    def score(self, query: str, doc: T) -> float:
+        """Return the score of a document for a query."""
+        return sum(self.word_score(q, doc) for q in words_normalize(query))
 
     def word_scores(self, word: str) -> dict[T, float]:
+        """Return all the non-zero scoring documents and theirs scores."""
         res = {}
         k_plus_1 = self.k + 1
         avgdl = self.corpus_words / len(self.corpus)
         freq_bias_add = 1 - self.b
         freq_bias_mul = self.b / avgdl
-        idf = self.idf(word)
+        idf = self._idf(word)
         for ident in self.word_usage.get(word, set()):
             doc_words, doc = self.corpus[ident]
             f_q = doc[word]
@@ -108,19 +121,18 @@ class BM25[T]:
             res[ident] = idf * quotient
         return res
 
-    def scores(self, querry: str) -> dict[T, float]:
-        # Old slow impl:
-        # return {k: self.score(querry, k) for k in self.corpus}
+    def scores(self, query: str) -> dict[T, float]:
+        """Return the non-zero-scoring documents and their score."""
         res: dict[T, float] = {}
-        for word in words_normalize(querry):
+        for word in words_normalize(query):
             for k, v in self.word_scores(word).items():
                 res[k] = res.get(k, 0.0) + v
         return res
 
-    def best_k(self, querry: str, k: int) -> list[T]:
-        k = min(len(self.corpus), k)
+    def best_k(self, query: str, k: int) -> list[T]:
+        """Return the k best non-zero scoring documents in the corpus."""
         scores = sorted(
-            self.scores(querry).items(),
+            self.scores(query).items(),
             key=lambda e: e[1],
             reverse=True,
         )[:k]
diff --git a/uv.lock b/uv.lock
index 6c12a842122d9d3f97a2d4f3d1dbb566f346257c..398cdb11161eddb6f40ca1129ca2ae6e64edefe2 100644 (file)
--- a/uv.lock
+++ b/uv.lock
@@ -775,6 +775,7 @@ dev = [
     { name = "flake8" },
     { name = "mypy" },
     { name = "ruff" },
+    { name = "ty" },
     { name = "types-tqdm" },
 ]
 
@@ -794,6 +795,7 @@ dev = [
     { name = "flake8", specifier = ">=7.3.0" },
     { name = "mypy", specifier = ">=2.3.1" },
     { name = "ruff", specifier = ">=0.16.4" },
+    { name = "ty", specifier = ">=0.0.75" },
     { name = "types-tqdm", specifier = ">=4.70.0.20260805" },
 ]
 
@@ -1104,6 +1106,31 @@ wheels = [
     { url = "https://files.pythonhosted.org/packages/41/c4/a12e1d9b387fb0c40a57116db82b457e8c771cb419163cda29204d74a595/transformers-5.15.1-py3-none-any.whl", hash = "sha256:b7cdf238ff583e3a58dbc7fa34da1aaf091ce063141f65a30538160bd5afe93f", size = 11749582, upload-time = "2026-08-19T11:28:16.726Z" },
 ]
 
+[[package]]
+name = "ty"
+version = "0.0.75"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/81/d0/d0c96f898d6974a4a3569ab3efdf9512c04ad99f9203effb55f72497fe97/ty-0.0.75.tar.gz", hash = "sha256:4c5eead33dfbf6e2ebb4f400f74b51ffc9bab702a6f23ddb648a1cbb740387e3", size = 6868326, upload-time = "2026-08-26T20:23:40.399Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/cb/6c/b12d03505f17581f0cfa3c12273fe34c1d67b36dfda1bc561a6bdc16512b/ty-0.0.75-py3-none-linux_armv6l.whl", hash = "sha256:e5409f50db2246fd4bd039d93d261e0cfa1daa554a4fb77256f91072c570349a", size = 12972606, upload-time = "2026-08-26T20:22:59.716Z" },
+    { url = "https://files.pythonhosted.org/packages/d1/aa/30f11eecd9215a9f87e8fe8baaf48f3ce905f5d75b8e4aac70f0091f130c/ty-0.0.75-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5e7b8b3472fb9bb2eeab314984b265df08a7a9d518867a9e6020eebc06570be2", size = 12527158, upload-time = "2026-08-26T20:23:02.767Z" },
+    { url = "https://files.pythonhosted.org/packages/f2/11/7fd7001b0b5c6610bfbad7357e47d5fe6f82d4e84e94c53776a478f5e9f8/ty-0.0.75-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c6ccf34169821fe0d23e3360deeef981d217963412f1d087b9bdd32ec57f7a57", size = 12400533, upload-time = "2026-08-26T20:23:04.965Z" },
+    { url = "https://files.pythonhosted.org/packages/fd/7f/1e284ea3d348d7be02f12d83bc22ed9ef193033f863f05b64db99027f141/ty-0.0.75-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:842ebb41e9c6c334b40768704e20b1a69d5c6b08805b289d5e0e2565f49f2de1", size = 12420592, upload-time = "2026-08-26T20:23:07.427Z" },
+    { url = "https://files.pythonhosted.org/packages/2d/ab/d813271543370c47fd74b5118f2066ab32b0983e907b1821f3f9a6d0fa7f/ty-0.0.75-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf7a5a723c5f1e0fab4ffbfe9bd95123a526ed48f206e5f25cb2161ca294007a", size = 12739219, upload-time = "2026-08-26T20:23:09.809Z" },
+    { url = "https://files.pythonhosted.org/packages/31/5b/95b49cc5570fd92a7bf63732f649b31906158721e03c7fcb1b5be74ee3bf/ty-0.0.75-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:54382f98e5da292fcd7104391afef5105c35bb2f312e29bea6f5fa419935255c", size = 13494046, upload-time = "2026-08-26T20:23:12.191Z" },
+    { url = "https://files.pythonhosted.org/packages/2c/0d/502d2dd68173cf020e1ad2bdbab9544c86776de0b0e2ed15f8c2fe006e3d/ty-0.0.75-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac13b180dc2aade2cd243f56b01650e78bf091a2e522ad3bc947245d7837c613", size = 13938899, upload-time = "2026-08-26T20:23:14.764Z" },
+    { url = "https://files.pythonhosted.org/packages/20/5b/f3b12a25c07224456219fc2bd20db0ad7e40b304be0ff6aad728da0135f9/ty-0.0.75-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:752df7951a443219d7f1ff817e3723c85d428565ff449e08a7a93ba821661526", size = 13656711, upload-time = "2026-08-26T20:23:17.145Z" },
+    { url = "https://files.pythonhosted.org/packages/51/7b/f090ad306e2b15a07b332d647138c5264b89d9758855ecce8b8a10bcb153/ty-0.0.75-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fd399feedf7cee816563c1baec45fc1c0b3c89f1ea42364920b688004b5b7da", size = 13093499, upload-time = "2026-08-26T20:23:19.489Z" },
+    { url = "https://files.pythonhosted.org/packages/f1/4b/f69b99aaaca0c7c65d5f114b186b26b21666f767b0c69eec99a2bdccc061/ty-0.0.75-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:d7625f6f56c7dc1e873579fdc9e432a0e21e302afe847ab60704d2303442a92e", size = 13520580, upload-time = "2026-08-26T20:23:21.789Z" },
+    { url = "https://files.pythonhosted.org/packages/b6/e7/692c5f905c0345a15d2255fc74066d660f030254ae8dcdaf33f5a5c2f279/ty-0.0.75-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:89e7d527e95a2534b70cae29e94c104b84082760ea05927d23bb87280969c104", size = 12524095, upload-time = "2026-08-26T20:23:24.026Z" },
+    { url = "https://files.pythonhosted.org/packages/ba/9a/f42b12cf265ea95344bf554764c4791cfb273bdd628aadd7c209af7cadc3/ty-0.0.75-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0843f134440740706e01bee5f88f4cfc10e9b018bddb9e4ef4c12dc9fc0c9aef", size = 12756591, upload-time = "2026-08-26T20:23:26.126Z" },
+    { url = "https://files.pythonhosted.org/packages/7c/5e/9b180c133cb9cce48179a7d2bf9e1802d992aa8176a918e0e05205760b42/ty-0.0.75-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1bd0ec0e50ee1376875c88891efe6f549c3560fa5b2ddad79a425cd5a6218b9c", size = 12998754, upload-time = "2026-08-26T20:23:28.353Z" },
+    { url = "https://files.pythonhosted.org/packages/39/f6/3c6ef5dd550103e29905121c67fb96a374564f31a2f44c6faa1af98c2d61/ty-0.0.75-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1f9eafd561f90110d5e29f589ec3e956c4686e2f6631348d99276436f5cbe4d1", size = 13316474, upload-time = "2026-08-26T20:23:30.857Z" },
+    { url = "https://files.pythonhosted.org/packages/bb/52/12776337874c821076bd5368e352ccd9e67174790abe3b856f749cb3524b/ty-0.0.75-py3-none-win32.whl", hash = "sha256:05063a6fafe2154b794a7f964515d148e51acd186d72d4a3acd347ee9fa19336", size = 12316315, upload-time = "2026-08-26T20:23:33.528Z" },
+    { url = "https://files.pythonhosted.org/packages/53/e6/bb51e16af5c7138c9f52f8f3d0a401a371c6798d092e3b74926f186a9814/ty-0.0.75-py3-none-win_amd64.whl", hash = "sha256:81cf1ba5f6b7536ad56747865214255d9bc8e80533a689dbb9ddeaad464b09f1", size = 12917267, upload-time = "2026-08-26T20:23:35.978Z" },
+    { url = "https://files.pythonhosted.org/packages/39/73/4542f829107468b5de4231af67f29927c093bfad11f3c1e5b2c08fb1206b/ty-0.0.75-py3-none-win_arm64.whl", hash = "sha256:541c9af5b7a0ad23d15ec315a7da81150833c359f48124ed3789ff25eacd6f42", size = 12711024, upload-time = "2026-08-26T20:23:38.159Z" },
+]
+
 [[package]]
 name = "typer"
 version = "0.27.1"