diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..310c199 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,32 @@ +name: test + +on: [push, workflow_dispatch] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - name: Setup uv + uses: astral-sh/setup-uv@v5 + + - name: Install dependencies + run: uv sync --no-group production + + - name: Run tests with coverage + run: | + uv run --no-group production -m coverage run --source=test2text -m unittest discover tests + uv run --no-group production -m coverage report --ignore-errors + + - name: Lint + run: uvx ruff check + + - name: Check formatting + run: uvx ruff check --select E --ignore "E402,E501" --fix diff --git a/.gitignore b/.gitignore index ecbfcaa..8540854 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ .idea -.venv \ No newline at end of file +.venv +.coverage diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..74d5dd5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,13 @@ +## Utilities commands + +Run linter from CI with automatic fixes if possible: + +```bash +uvx ruff check --fix +``` + +Automatically format code: + +```bash +uvx ruff format +``` diff --git a/README.md b/README.md index b815621..283ad0e 100644 --- a/README.md +++ b/README.md @@ -97,4 +97,4 @@ erDiagram `CasesToAnnos` table. 5. Create report about the coverage of the requirements by the test cases: - By running your SQL queries in `requirements.db` SQLite database. - - Or by running `report.py` script. \ No newline at end of file + - Or by running `report.py` script. diff --git a/index_annotations.py b/index_annotations.py index 4db6d37..cff514f 100644 --- a/index_annotations.py +++ b/index_annotations.py @@ -9,18 +9,22 @@ BATCH_SIZE = 100 -if __name__ == '__main__': - db = DbClient('./private/requirements.db') - annotations_folder = Path('./private/annotations') +if __name__ == "__main__": + db = DbClient("./private/requirements.db") + annotations_folder = Path("./private/annotations") # Write annotations to the database for i, file in enumerate(os.listdir(annotations_folder)): - logging.info(f'Processing file {i + 1}: {file}') - with open(annotations_folder / file, newline='', encoding='utf-8', mode='r') as csvfile: + logging.info(f"Processing file {i + 1}: {file}") + with open( + annotations_folder / file, newline="", encoding="utf-8", mode="r" + ) as csvfile: reader = csv.reader(csvfile) for row in reader: [summary, _, test_script, test_case, *_] = row - anno_id = db.annotations.insert(summary=summary) - tc_id = db.test_cases.insert(test_script=test_script, test_case=test_case) + anno_id = db.annotations.get_or_insert(summary=summary) + tc_id = db.test_cases.get_or_insert( + test_script=test_script, test_case=test_case + ) db.cases_to_annos.insert(case_id=tc_id, annotation_id=anno_id) db.conn.commit() # Embed annotations @@ -38,17 +42,20 @@ def write_batch(): embeddings = embed_annotations_batch([annotation for _, annotation in batch]) for i, (anno_id, annotation) in enumerate(batch): embedding = embeddings[i] - db.conn.execute(""" + db.conn.execute( + """ UPDATE Annotations SET embedding = ? WHERE id = ? - """, (embedding, anno_id)) + """, + (embedding, anno_id), + ) db.conn.commit() batch = [] for i, (anno_id, summary) in enumerate(annotations.fetchall()): if i % 100 == 0: - logging.info(f'Processing annotation {i + 1}/{annotations_count}') + logging.info(f"Processing annotation {i + 1}/{annotations_count}") batch.append((anno_id, summary)) if len(batch) == BATCH_SIZE: write_batch() @@ -57,4 +64,4 @@ def write_batch(): cursor = db.conn.execute(""" SELECT COUNT(*) FROM Annotations """) - print(cursor.fetchone()) \ No newline at end of file + print(cursor.fetchone()) diff --git a/index_requirements.py b/index_requirements.py index 2b034be..7401c1d 100644 --- a/index_requirements.py +++ b/index_requirements.py @@ -1,31 +1,38 @@ import csv import logging + logging.basicConfig(level=logging.DEBUG) from test2text.db import DbClient from test2text.embeddings.embed import embed_requirements_batch BATCH_SIZE = 100 -if __name__ == '__main__': - db = DbClient('./private/requirements.db') +if __name__ == "__main__": + db = DbClient("./private/requirements.db") # Index requirements - with open('./private/TRACEABILITY MATRIX.csv', newline='', encoding='utf-8', mode='r') as csvfile: + with open( + "./private/TRACEABILITY MATRIX.csv", newline="", encoding="utf-8", mode="r" + ) as csvfile: reader = csv.reader(csvfile) for _ in range(3): next(reader) batch = [] - last_requirement = '' + last_requirement = "" + def write_batch(): global batch - embeddings = embed_requirements_batch([requirement for _, requirement in batch]) + embeddings = embed_requirements_batch( + [requirement for _, requirement in batch] + ) for i, (external_id, requirement) in enumerate(batch): embedding = embeddings[i] db.requirements.insert(requirement, embedding, external_id) db.conn.commit() batch = [] + for row in reader: [external_id, requirement, *_] = row - if requirement.startswith('...'): + if requirement.startswith("..."): requirement = last_requirement + requirement[3:] last_requirement = requirement batch.append((external_id, requirement)) @@ -36,4 +43,4 @@ def write_batch(): cursor = db.conn.execute(""" SELECT COUNT(*) FROM Requirements """) - print(cursor.fetchone()) \ No newline at end of file + print(cursor.fetchone()) diff --git a/link_reqs_to_annos.py b/link_reqs_to_annos.py index 9c9460c..5a6829e 100644 --- a/link_reqs_to_annos.py +++ b/link_reqs_to_annos.py @@ -1,10 +1,11 @@ import logging + logging.basicConfig(level=logging.INFO) logger = logging.getLogger() from test2text.db import DbClient -if __name__ == '__main__': - db = DbClient('./private/requirements.db') +if __name__ == "__main__": + db = DbClient("./private/requirements.db") db.annos_to_reqs.recreate_table() # Link requirements to annotations annotations = db.conn.execute(""" @@ -17,7 +18,7 @@ """) # Visualize distances distances = [] - logger.info('Processing distances') + logger.info("Processing distances") current_req_id = None current_req_annos = 0 for i, (anno_id, req_id, distance) in enumerate(annotations.fetchall()): @@ -26,9 +27,12 @@ current_req_id = req_id current_req_annos = 0 if current_req_annos < 5 or distance < 0.7: - db.annos_to_reqs.insert(annotation_id=anno_id, requirement_id=req_id, cached_distance=distance) + db.annos_to_reqs.insert( + annotation_id=anno_id, requirement_id=req_id, cached_distance=distance + ) current_req_annos += 1 db.conn.commit() import matplotlib.pyplot as plt + plt.hist(distances, bins=100) - plt.savefig('./private/distances.png') \ No newline at end of file + plt.savefig("./private/distances.png") diff --git a/pyproject.toml b/pyproject.toml index a0ad8a7..e15b322 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,14 +8,28 @@ authors = [ readme = "README.md" requires-python = ">=3.9" dependencies = [ - "einops>=0.8.1", "matplotlib>=3.9.4", - "sentence-transformers>=4.0.1", "sqlite-vec>=0.1.6", - "tabbyset>=1.0.0", - "torch", + "tabbyset>=1.0.0" ] +[dependency-groups] +dev = [ + "coverage>=7.9.2", + "ruff>=0.12.3", +] +production = [ + "einops>=0.8.1", + "sentence-transformers>=4.0.1", + "torch" +] + +[tool.uv] +default-groups = "all" + +[tool.ruff.lint] +ignore = ["E402"] + [tool.uv.sources] torch = {index = "pytorch-cpu"} diff --git a/report.py b/report.py index 153784e..ef4fc2a 100644 --- a/report.py +++ b/report.py @@ -1,13 +1,13 @@ -from sympy import limit - from test2text.db import DbClient from tqdm import tqdm + def add_new_line(summary): - return summary.replace('\n', '
') + return summary.replace("\n", "
") + -if __name__ == '__main__': - with open('./private/report.html', 'w', newline='', encoding='utf-8') as f: +if __name__ == "__main__": + with open("./private/report.html", "w", newline="", encoding="utf-8") as f: f.write(""" @@ -18,20 +18,32 @@ def add_new_line(summary):
-
+
""") - db = DbClient('./private/requirements.db') - all_reqs_count = db.conn.execute('SELECT COUNT(*) FROM Requirements').fetchone()[0] + db = DbClient("./private/requirements.db") + all_reqs_count = db.conn.execute( + "SELECT COUNT(*) FROM Requirements" + ).fetchone()[0] f.write('') + f.write("") data = db.conn.execute(""" SELECT @@ -60,9 +72,12 @@ def add_new_line(summary): current_req_id = None current_annotations = {} current_test_scripts = set() - progress_bar = tqdm(total=all_reqs_count, desc='Generating report', unit='requirements') + progress_bar = tqdm( + total=all_reqs_count, desc="Generating report", unit="requirements" + ) written_count = 0 + def write_requirement(): global written_count # if written_count > 5: @@ -77,15 +92,26 @@ def write_requirement(): """) for anno_id, (anno_summary, distance) in current_annotations.items(): f.write( - f'
  • Annotation {anno_id} (distance: {distance:.3f}):

    {add_new_line(anno_summary)}

  • ') - f.write('') - f.write('

    Test Scripts

      ') + f"
    • Annotation {anno_id} (distance: {distance:.3f}):

      {add_new_line(anno_summary)}

    • " + ) + f.write("
    ") + f.write("

    Test Scripts

      ") for test_script in current_test_scripts: f.write(f"
    • {test_script}
    • ") - f.write('
    ') + f.write("") for row in data.fetchall(): - req_id, req_external_id, req_summary, anno_id, anno_summary, distance, case_id, test_script, test_case = row + ( + req_id, + req_external_id, + req_summary, + anno_id, + anno_summary, + distance, + case_id, + test_script, + test_case, + ) = row if req_id != current_req_id: if current_req_id is not None: write_requirement() @@ -102,4 +128,4 @@ def write_requirement():
    - """) \ No newline at end of file + """) diff --git a/test2text/db/__init__.py b/test2text/db/__init__.py index 824b4c6..60f022d 100644 --- a/test2text/db/__init__.py +++ b/test2text/db/__init__.py @@ -1 +1,2 @@ -from .client import DbClient \ No newline at end of file +__all__ = ["DbClient"] +from .client import DbClient diff --git a/test2text/db/client.py b/test2text/db/client.py index 89686f4..efdfa27 100644 --- a/test2text/db/client.py +++ b/test2text/db/client.py @@ -2,28 +2,54 @@ import sqlite_vec import logging -from .tables import RequirementsTable, AnnotationsTable, AnnotationsToRequirementsTable, TestCasesTable, TestCasesToAnnotationsTable +from test2text.utils.semver import Semver +from .tables import ( + RequirementsTable, + AnnotationsTable, + AnnotationsToRequirementsTable, + TestCasesTable, + TestCasesToAnnotationsTable, +) from ..utils.path import PathParam logger = logging.getLogger(__name__) + class DbClient: conn: sqlite3.Connection + @staticmethod + def _check_sqlite_version(): + # Version when RETURNED is available + REQUIRED_SQLITE_VERSION = Semver("3.35.0") + sqlite_version = Semver(sqlite3.sqlite_version) + if sqlite_version < REQUIRED_SQLITE_VERSION: + raise RuntimeError( + f"SQLite version {sqlite_version} is too old. " + f"Required version is {REQUIRED_SQLITE_VERSION}. " + "Please upgrade SQLite in your system to use test2text." + ) + def __init__(self, file_path: PathParam, embedding_dim: int = 768): - logger.info('Connecting to database at %s', file_path) + self._check_sqlite_version() + logger.info("Connecting to database at %s", file_path) self.conn = sqlite3.connect(file_path) self.embedding_dim = embedding_dim + self._turn_on_foreign_keys() self._install_extension() self._init_tables() - logger.info('Connected to database at %s', file_path) + logger.info("Connected to database at %s", file_path) def _install_extension(self): self.conn.enable_load_extension(True) - logger.debug('Installing sqlite_vec extension') + logger.debug("Installing sqlite_vec extension") sqlite_vec.load(self.conn) self.conn.enable_load_extension(False) + def _turn_on_foreign_keys(self): + self.conn.execute("PRAGMA foreign_keys = ON") + logger.debug("Foreign keys enabled") + def _init_tables(self): self.requirements = RequirementsTable(self.conn, self.embedding_dim) self.annotations = AnnotationsTable(self.conn, self.embedding_dim) @@ -34,4 +60,4 @@ def _init_tables(self): self.annotations.init_table() self.test_cases.init_table() self.annos_to_reqs.init_table() - self.cases_to_annos.init_table() \ No newline at end of file + self.cases_to_annos.init_table() diff --git a/test2text/db/tables/__init__.py b/test2text/db/tables/__init__.py index 7a14766..8119062 100644 --- a/test2text/db/tables/__init__.py +++ b/test2text/db/tables/__init__.py @@ -1,6 +1,14 @@ +__all__ = [ + "AbstractTable", + "AnnotationsTable", + "RequirementsTable", + "AnnotationsToRequirementsTable", + "TestCasesTable", + "TestCasesToAnnotationsTable", +] from .abstract_table import AbstractTable from .annotations import AnnotationsTable from .requirements import RequirementsTable from .annos_to_reqs import AnnotationsToRequirementsTable from .test_case import TestCasesTable -from .cases_to_annos import TestCasesToAnnotationsTable \ No newline at end of file +from .cases_to_annos import TestCasesToAnnotationsTable diff --git a/test2text/db/tables/abstract_table.py b/test2text/db/tables/abstract_table.py index 8f3bd21..864575e 100644 --- a/test2text/db/tables/abstract_table.py +++ b/test2text/db/tables/abstract_table.py @@ -9,4 +9,3 @@ def __init__(self, connection: Connection): @abstractmethod def init_table(self): pass - diff --git a/test2text/db/tables/annos_to_reqs.py b/test2text/db/tables/annos_to_reqs.py index cecd7ad..06b32b8 100644 --- a/test2text/db/tables/annos_to_reqs.py +++ b/test2text/db/tables/annos_to_reqs.py @@ -1,5 +1,7 @@ +import sqlite3 from .abstract_table import AbstractTable + class AnnotationsToRequirementsTable(AbstractTable): def init_table(self): self.connection.execute(""" @@ -19,11 +21,29 @@ def recreate_table(self): """) self.init_table() - def insert(self, annotation_id: int, requirement_id: int, cached_distance: float): - self.connection.execute( - """ - INSERT OR IGNORE INTO AnnotationsToRequirements (annotation_id, requirement_id, cached_distance) - VALUES (?, ?, ?) - """, - (annotation_id, requirement_id, cached_distance) - ) \ No newline at end of file + def insert( + self, annotation_id: int, requirement_id: int, cached_distance: float + ) -> bool: + try: + cursor = self.connection.execute( + """ + INSERT OR IGNORE INTO AnnotationsToRequirements (annotation_id, requirement_id, cached_distance) + VALUES (?, ?, ?) + RETURNING true + """, + (annotation_id, requirement_id, cached_distance), + ) + result = cursor.fetchone() + cursor.close() + if result: + return result[0] + except sqlite3.IntegrityError: + # If the insert fails due to a duplicate, we simply ignore it + pass + return False + + def count(self) -> int: + cursor = self.connection.execute( + "SELECT COUNT(*) FROM AnnotationsToRequirements" + ) + return cursor.fetchone()[0] diff --git a/test2text/db/tables/annotations.py b/test2text/db/tables/annotations.py index a3b3324..19ede4e 100644 --- a/test2text/db/tables/annotations.py +++ b/test2text/db/tables/annotations.py @@ -11,7 +11,6 @@ def __init__(self, connection: Connection, embedding_size: int): super().__init__(connection) self.embedding_size = embedding_size - def init_table(self): self.connection.execute( Template(""" @@ -22,32 +21,40 @@ def init_table(self): CHECK ( typeof(embedding) == 'null' or - (typeof(embedding) == 'blob' and vec_length(embedding) == $embedding_size) + (typeof(embedding) == 'blob' + and vec_length(embedding) == $embedding_size) ) ) """).substitute(embedding_size=self.embedding_size) ) - def insert(self, summary: str, embedding: list[float] = None) -> int: + def insert(self, summary: str, embedding: list[float] = None) -> Optional[int]: cursor = self.connection.execute( """ INSERT OR IGNORE INTO Annotations (summary, embedding) VALUES (?, ?) RETURNING id """, - (summary, serialize_float32(embedding) if embedding is not None else None) + (summary, serialize_float32(embedding) if embedding is not None else None), ) result = cursor.fetchone() cursor.close() if result: return result[0] + else: + return None + + def get_or_insert(self, summary: str, embedding: list[float] = None) -> int: + inserted_id = self.insert(summary, embedding) + if inserted_id is not None: + return inserted_id else: cursor = self.connection.execute( """ SELECT id FROM Annotations WHERE summary = ? """, - (summary,) + (summary,), ) result = cursor.fetchone() cursor.close() diff --git a/test2text/db/tables/cases_to_annos.py b/test2text/db/tables/cases_to_annos.py index d8a91c4..f0b7e9c 100644 --- a/test2text/db/tables/cases_to_annos.py +++ b/test2text/db/tables/cases_to_annos.py @@ -1,5 +1,8 @@ +import sqlite3 + from .abstract_table import AbstractTable + class TestCasesToAnnotationsTable(AbstractTable): def init_table(self): self.connection.execute(""" @@ -12,11 +15,31 @@ def init_table(self): ) """) - def insert(self, case_id: int, annotation_id: int): - self.connection.execute( - """ - INSERT OR IGNORE INTO CasesToAnnos (case_id, annotation_id) - VALUES (?, ?) - """, - (case_id, annotation_id) - ) \ No newline at end of file + def recreate_table(self): + self.connection.execute(""" + DROP TABLE IF EXISTS CasesToAnnos + """) + self.init_table() + + def insert(self, case_id: int, annotation_id: int) -> bool: + try: + cursor = self.connection.execute( + """ + INSERT OR IGNORE INTO CasesToAnnos (case_id, annotation_id) + VALUES (?, ?) + RETURNING true + """, + (case_id, annotation_id), + ) + result = cursor.fetchone() + cursor.close() + if result: + return result[0] + except sqlite3.IntegrityError: + # If the insert fails due to a duplicate, we simply ignore it + pass + return False + + def count(self) -> int: + cursor = self.connection.execute("SELECT COUNT(*) FROM CasesToAnnos") + return cursor.fetchone()[0] diff --git a/test2text/db/tables/requirements.py b/test2text/db/tables/requirements.py index 1f0e581..91420f3 100644 --- a/test2text/db/tables/requirements.py +++ b/test2text/db/tables/requirements.py @@ -22,20 +22,27 @@ def init_table(self): CHECK ( typeof(embedding) == 'null' or - (typeof(embedding) == 'blob' and vec_length(embedding) == $embedding_size) + (typeof(embedding) == 'blob' + and vec_length(embedding) == $embedding_size) ) ) """).substitute(embedding_size=self.embedding_size) ) - def insert(self, summary: str, embedding: list[float] = None, external_id: str = None) -> Optional[int]: + def insert( + self, summary: str, embedding: list[float] = None, external_id: str = None + ) -> Optional[int]: cursor = self.connection.execute( """ INSERT OR IGNORE INTO Requirements (summary, embedding, external_id) VALUES (?, ?, ?) RETURNING id """, - (summary, serialize_float32(embedding) if embedding is not None else None, external_id) + ( + summary, + serialize_float32(embedding) if embedding is not None else None, + external_id, + ), ) result = cursor.fetchone() if result: diff --git a/test2text/db/tables/test_case.py b/test2text/db/tables/test_case.py index 7d3f6e9..646acda 100644 --- a/test2text/db/tables/test_case.py +++ b/test2text/db/tables/test_case.py @@ -1,5 +1,8 @@ +from typing import Optional + from .abstract_table import AbstractTable + class TestCasesTable(AbstractTable): def init_table(self): self.connection.execute(""" @@ -11,27 +14,34 @@ def init_table(self): ) """) - def insert(self, test_script: str, test_case: str): + def insert(self, test_script: str, test_case: str) -> Optional[int]: cursor = self.connection.execute( """ INSERT OR IGNORE INTO TestCases (test_script, test_case) VALUES (?, ?) RETURNING id """, - (test_script, test_case) + (test_script, test_case), ) result = cursor.fetchone() cursor.close() if result: return result[0] + else: + return None + + def get_or_insert(self, test_script: str, test_case: str) -> int: + inserted_id = self.insert(test_script, test_case) + if inserted_id is not None: + return inserted_id else: cursor = self.connection.execute( """ SELECT id FROM TestCases WHERE test_script = ? AND test_case = ? """, - (test_script, test_case) + (test_script, test_case), ) result = cursor.fetchone() cursor.close() - return result[0] \ No newline at end of file + return result[0] diff --git a/test2text/embeddings/embed.py b/test2text/embeddings/embed.py index 38d764b..8497403 100644 --- a/test2text/embeddings/embed.py +++ b/test2text/embeddings/embed.py @@ -1,20 +1,27 @@ from sentence_transformers import SentenceTransformer import numpy as np import logging + logger = logging.getLogger() -logger.info('Loading model') +logger.info("Loading model") model = SentenceTransformer("nomic-ai/nomic-embed-text-v1", trust_remote_code=True) -logger.info('Model loaded') +logger.info("Model loaded") + def embed_requirement(requirement: str) -> np.ndarray: - return model.encode(['search_document: ' + requirement])[0] + return model.encode(["search_document: " + requirement])[0] + def embed_requirements_batch(requirements: list[str]) -> np.ndarray: - return model.encode(['search_document: ' + requirement for requirement in requirements]) + return model.encode( + ["search_document: " + requirement for requirement in requirements] + ) + def embed_annotation(annotation: str) -> np.ndarray: - return model.encode(['search_query: ' + annotation])[0] + return model.encode(["search_query: " + annotation])[0] + def embed_annotations_batch(annotations: list[str]) -> np.ndarray: - return model.encode(['search_query: ' + annotation for annotation in annotations]) + return model.encode(["search_query: " + annotation for annotation in annotations]) diff --git a/test2text/utils/__init__.py b/test2text/utils/__init__.py index 031999d..ec7572b 100644 --- a/test2text/utils/__init__.py +++ b/test2text/utils/__init__.py @@ -1,2 +1,3 @@ +__all__ = ["unpack_float32", "PathParam"] from .sqlite_vec import unpack_float32 -from .path import PathParam \ No newline at end of file +from .path import PathParam diff --git a/test2text/utils/path.py b/test2text/utils/path.py index 652f4b3..2805a77 100644 --- a/test2text/utils/path.py +++ b/test2text/utils/path.py @@ -2,4 +2,4 @@ from pathlib import Path from typing import Union -PathParam = Union[str, Path, PathLike] \ No newline at end of file +PathParam = Union[str, Path, PathLike] diff --git a/test2text/utils/semver.py b/test2text/utils/semver.py new file mode 100644 index 0000000..820b2a2 --- /dev/null +++ b/test2text/utils/semver.py @@ -0,0 +1,76 @@ +class Semver: + """ + A class to represent a semantic version. + """ + + def __init__(self, version: str): + self.major, self.minor, self.patch = (int(v) for v in version.split(".")) + + def __str__(self): + return f"{self.major}.{self.minor}.{self.patch}" + + def __eq__(self, other): + if isinstance(other, Semver): + return (self.major, self.minor, self.patch) == ( + other.major, + other.minor, + other.patch, + ) + if isinstance(other, str): + return str(self) == other + return False + + def __lt__(self, other): + if isinstance(other, Semver): + return (self.major, self.minor, self.patch) < ( + other.major, + other.minor, + other.patch, + ) + if isinstance(other, str): + return self < Semver(other) + return NotImplemented + + def __le__(self, other): + if isinstance(other, Semver): + return (self.major, self.minor, self.patch) <= ( + other.major, + other.minor, + other.patch, + ) + if isinstance(other, str): + return self <= Semver(other) + return NotImplemented + + def __gt__(self, other): + if isinstance(other, Semver): + return (self.major, self.minor, self.patch) > ( + other.major, + other.minor, + other.patch, + ) + if isinstance(other, str): + return self > Semver(other) + return NotImplemented + + def __ge__(self, other): + if isinstance(other, Semver): + return (self.major, self.minor, self.patch) >= ( + other.major, + other.minor, + other.patch, + ) + if isinstance(other, str): + return self >= Semver(other) + return NotImplemented + + def __ne__(self, other): + if isinstance(other, Semver): + return (self.major, self.minor, self.patch) != ( + other.major, + other.minor, + other.patch, + ) + if isinstance(other, str): + return str(self) != other + return NotImplemented diff --git a/test2text/utils/sqlite_vec.py b/test2text/utils/sqlite_vec.py index f7ba21d..fd1265d 100644 --- a/test2text/utils/sqlite_vec.py +++ b/test2text/utils/sqlite_vec.py @@ -1,5 +1,6 @@ def unpack_float32(data: bytes) -> list[float]: """Deserializes the "raw bytes" format into a list of floats""" from struct import unpack + num_floats = len(data) // 4 # each float32 is 4 bytes - return list(unpack("%sf" % num_floats, data)) \ No newline at end of file + return list(unpack("%sf" % num_floats, data)) diff --git a/tests/test_db/__init__.py b/tests/test_db/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_db/test_db_client.py b/tests/test_db/test_db_client.py new file mode 100644 index 0000000..ca1027b --- /dev/null +++ b/tests/test_db/test_db_client.py @@ -0,0 +1,10 @@ +from unittest import TestCase +from test2text.db.client import DbClient + + +class TestDBClient(TestCase): + def test_db_client(self): + db = DbClient(":memory:") + with self.subTest("extensions"): + (vec_version,) = db.conn.execute("select vec_version()").fetchone() + self.assertIsNotNone(vec_version) diff --git a/tests/test_db/test_tables/__init__.py b/tests/test_db/test_tables/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_db/test_tables/test_annos_to_reqs.py b/tests/test_db/test_tables/test_annos_to_reqs.py new file mode 100644 index 0000000..33f3f5b --- /dev/null +++ b/tests/test_db/test_tables/test_annos_to_reqs.py @@ -0,0 +1,52 @@ +from unittest import TestCase +from test2text.db.client import DbClient + + +class TestAnnosToReqsTable(TestCase): + def setUp(self): + self.db = DbClient(":memory:") + self.anno1 = self.db.annotations.insert("Test Annotation 1") + self.anno2 = self.db.annotations.insert("Test Annotation 2") + self.req1 = self.db.requirements.insert("Test Requirement 1") + self.req2 = self.db.requirements.insert("Test Requirement 2") + self.wrong_anno = 9999 + self.wrong_req = 8888 + + def test_insert_single(self): + count_before = self.db.annos_to_reqs.count() + inserted = self.db.annos_to_reqs.insert(self.anno1, self.req1, 1) + count_after = self.db.annos_to_reqs.count() + self.assertTrue(inserted) + self.assertEqual(count_after, count_before + 1) + + def test_insert_multiple(self): + count_before = self.db.annos_to_reqs.count() + inserted1 = self.db.annos_to_reqs.insert(self.anno1, self.req1, 1) + inserted2 = self.db.annos_to_reqs.insert(self.anno2, self.req2, 1) + count_after = self.db.annos_to_reqs.count() + self.assertTrue(inserted1) + self.assertTrue(inserted2) + self.assertEqual(count_after, count_before + 2) + + def test_insert_duplicate(self): + count_before = self.db.annos_to_reqs.count() + inserted1 = self.db.annos_to_reqs.insert(self.anno1, self.req1, 1) + inserted2 = self.db.annos_to_reqs.insert(self.anno1, self.req1, 1) + count_after = self.db.annos_to_reqs.count() + self.assertTrue(inserted1) + self.assertFalse(inserted2) # Second insertion should fail as it's a duplicate + self.assertEqual(count_after, count_before + 1) + + def test_insert_wrong_annotation(self): + count_before = self.db.annos_to_reqs.count() + inserted = self.db.annos_to_reqs.insert(self.wrong_anno, self.req1, 1) + count_after = self.db.annos_to_reqs.count() + self.assertFalse(inserted) # Should fail due to foreign key constraint + self.assertEqual(count_after, count_before) + + def test_insert_wrong_requirement(self): + count_before = self.db.annos_to_reqs.count() + inserted = self.db.annos_to_reqs.insert(self.anno1, self.wrong_req, 1) + count_after = self.db.annos_to_reqs.count() + self.assertFalse(inserted) # Should fail due to foreign key constraint + self.assertEqual(count_before, count_after) diff --git a/tests/test_db/test_tables/test_annotations.py b/tests/test_db/test_tables/test_annotations.py new file mode 100644 index 0000000..e398e47 --- /dev/null +++ b/tests/test_db/test_tables/test_annotations.py @@ -0,0 +1,57 @@ +from unittest import TestCase +from test2text.db.client import DbClient + + +class TestAnnotationsTable(TestCase): + def setUp(self): + self.db = DbClient(":memory:") + + def test_insert_single(self): + id1 = self.db.annotations.insert("Test Summary 1") + self.assertIsNotNone(id1) + + def test_insert_multiple(self): + id1 = self.db.annotations.insert("Test Summary 1") + id2 = self.db.annotations.insert("Test Summary 2") + self.assertIsNotNone(id1) + self.assertIsNotNone(id2) + self.assertNotEqual(id1, id2) + + def test_insert_duplicate(self): + id1 = self.db.annotations.insert("Test Summary 2") + id2 = self.db.annotations.insert("Test Summary 2") + self.assertIsNotNone(id1) + self.assertIsNone(id2) + + def test_insert_embedding(self): + embedding = [0.1] * self.db.annotations.embedding_size + id1 = self.db.annotations.insert("Test Summary 3", embedding) + self.assertIsNotNone(id1) + + def test_insert_short_embedding(self): + short_embedding = [0.1] * (self.db.annotations.embedding_size - 1) + id1 = self.db.annotations.insert("Test Summary 4", short_embedding) + self.assertIsNone(id1) + + def test_insert_long_embedding(self): + long_embedding = [0.1] * (self.db.annotations.embedding_size + 1) + id1 = self.db.annotations.insert("Test Summary 5", long_embedding) + self.assertIsNone(id1) + + def test_get_or_insert_single(self): + id1 = self.db.annotations.get_or_insert("Test Summary 6") + self.assertIsNotNone(id1) + + def test_get_or_insert_multiple(self): + id1 = self.db.annotations.get_or_insert("Test Summary 7") + id2 = self.db.annotations.get_or_insert("Test Summary 8") + self.assertIsNotNone(id1) + self.assertIsNotNone(id2) + self.assertNotEqual(id1, id2) + + def test_get_or_insert_duplicate(self): + id1 = self.db.annotations.get_or_insert("Test Summary 9") + id2 = self.db.annotations.get_or_insert("Test Summary 9") + self.assertIsNotNone(id1) + self.assertIsNotNone(id2) + self.assertEqual(id1, id2) diff --git a/tests/test_db/test_tables/test_cases_to_annos.py b/tests/test_db/test_tables/test_cases_to_annos.py new file mode 100644 index 0000000..9e160db --- /dev/null +++ b/tests/test_db/test_tables/test_cases_to_annos.py @@ -0,0 +1,52 @@ +from unittest import TestCase +from test2text.db.client import DbClient + + +class TestCasesToAnnosTable(TestCase): + def setUp(self): + self.db = DbClient(":memory:") + self.case1 = self.db.test_cases.insert("Test Script 1", "Test Case 1") + self.case2 = self.db.test_cases.insert("Test Script 1", "Test Case 2") + self.anno1 = self.db.annotations.insert("Test Annotation 1") + self.anno2 = self.db.annotations.insert("Test Annotation 2") + self.wrong_case = 9999 + self.wrong_anno = 8888 + + def test_insert_single(self): + count_before = self.db.cases_to_annos.count() + inserted = self.db.cases_to_annos.insert(self.case1, self.anno1) + count_after = self.db.cases_to_annos.count() + self.assertTrue(inserted) + self.assertEqual(count_after, count_before + 1) + + def test_insert_multiple(self): + count_before = self.db.cases_to_annos.count() + inserted1 = self.db.cases_to_annos.insert(self.case1, self.anno1) + inserted2 = self.db.cases_to_annos.insert(self.case2, self.anno2) + count_after = self.db.cases_to_annos.count() + self.assertTrue(inserted1) + self.assertTrue(inserted2) + self.assertEqual(count_after, count_before + 2) + + def test_insert_duplicate(self): + count_before = self.db.cases_to_annos.count() + inserted1 = self.db.cases_to_annos.insert(self.case1, self.anno1) + inserted2 = self.db.cases_to_annos.insert(self.case1, self.anno1) + count_after = self.db.cases_to_annos.count() + self.assertTrue(inserted1) + self.assertFalse(inserted2) # Second insertion should fail as it's a duplicate + self.assertEqual(count_after, count_before + 1) + + def test_insert_wrong_case(self): + count_before = self.db.cases_to_annos.count() + inserted = self.db.cases_to_annos.insert(self.wrong_case, self.anno1) + count_after = self.db.cases_to_annos.count() + self.assertFalse(inserted) # Should fail due to foreign key constraint + self.assertEqual(count_after, count_before) + + def test_insert_wrong_annotation(self): + count_before = self.db.cases_to_annos.count() + inserted = self.db.cases_to_annos.insert(self.case1, self.wrong_anno) + count_after = self.db.cases_to_annos.count() + self.assertFalse(inserted) # Should fail due to foreign key constraint + self.assertEqual(count_before, count_after) diff --git a/tests/test_db/test_tables/test_requirements.py b/tests/test_db/test_tables/test_requirements.py new file mode 100644 index 0000000..94c1627 --- /dev/null +++ b/tests/test_db/test_tables/test_requirements.py @@ -0,0 +1,47 @@ +from unittest import TestCase +from test2text.db.client import DbClient + + +class TestRequirementsTable(TestCase): + def setUp(self): + self.db = DbClient(":memory:") + + def test_insert_single(self): + id1 = self.db.requirements.insert("Test Requirement 1") + self.assertIsNotNone(id1) + + def test_insert_multiple(self): + id1 = self.db.requirements.insert("Test Requirement 2") + id2 = self.db.requirements.insert("Test Requirement 3") + self.assertIsNotNone(id1) + self.assertIsNotNone(id2) + self.assertNotEqual(id1, id2) + + def test_insert_duplicate(self): + id1 = self.db.requirements.insert("Test Requirement 4") + id2 = self.db.requirements.insert("Test Requirement 4") + self.assertIsNotNone(id1) + self.assertIsNone(id2) + + def test_insert_with_external_id_single(self): + id1 = self.db.requirements.insert("Test Requirement 1", external_id="ext-1") + self.assertIsNotNone(id1) + + def test_insert_with_external_id_multiple(self): + id1 = self.db.requirements.insert("Test Requirement 2", external_id="ext-2") + id2 = self.db.requirements.insert("Test Requirement 3", external_id="ext-3") + self.assertIsNotNone(id1) + self.assertIsNotNone(id2) + self.assertNotEqual(id1, id2) + + def test_insert_with_external_id_duplicate(self): + id1 = self.db.requirements.insert("Test Requirement 4", external_id="ext-4") + id2 = self.db.requirements.insert("Test Requirement 4", external_id="ext-4") + self.assertIsNotNone(id1) + self.assertIsNone(id2) + + def test_insert_duplicate_external_id(self): + id1 = self.db.requirements.insert("Test Requirement 2", external_id="ext-2") + id2 = self.db.requirements.insert("Test Requirement 3", external_id="ext-2") + self.assertIsNotNone(id1) + self.assertIsNone(id2) diff --git a/tests/test_db/test_tables/test_test_cases.py b/tests/test_db/test_tables/test_test_cases.py new file mode 100644 index 0000000..ae0e293 --- /dev/null +++ b/tests/test_db/test_tables/test_test_cases.py @@ -0,0 +1,44 @@ +from unittest import TestCase +from test2text.db.client import DbClient + + +class TestTestCasesTable(TestCase): + def setUp(self): + self.db = DbClient(":memory:") + + def test_insert_single(self): + id1 = self.db.test_cases.insert("Test Script 1", "Test Case 1") + self.assertIsNotNone(id1) + self.assertIsInstance(id1, int) + + def test_insert_multiple(self): + id1 = self.db.test_cases.insert("Test Script 2", "Test Case 2") + id2 = self.db.test_cases.insert("Test Script 3", "Test Case 3") + self.assertIsNotNone(id1) + self.assertIsNotNone(id2) + self.assertNotEqual(id1, id2) + + def test_insert_duplicate(self): + id1 = self.db.test_cases.insert("Test Script 4", "Test Case 4") + id2 = self.db.test_cases.insert("Test Script 4", "Test Case 4") + self.assertIsNotNone(id1) + self.assertIsNone(id2) + + def test_get_or_insert_single(self): + id1 = self.db.test_cases.get_or_insert("Test Script 8", "Test Case 8") + self.assertIsNotNone(id1) + self.assertIsInstance(id1, int) + + def test_get_or_insert_multiple(self): + id1 = self.db.test_cases.get_or_insert("Test Script 9", "Test Case 9") + id2 = self.db.test_cases.get_or_insert("Test Script 10", "Test Case 10") + self.assertIsNotNone(id1) + self.assertIsNotNone(id2) + self.assertNotEqual(id1, id2) + + def test_get_or_insert_duplicate(self): + id1 = self.db.test_cases.get_or_insert("Test Script 11", "Test Case 11") + id2 = self.db.test_cases.get_or_insert("Test Script 11", "Test Case 11") + self.assertIsNotNone(id1) + self.assertIsNotNone(id2) + self.assertEqual(id1, id2) diff --git a/tests/test_db_client.py b/tests/test_db_client.py deleted file mode 100644 index ba6af2c..0000000 --- a/tests/test_db_client.py +++ /dev/null @@ -1,26 +0,0 @@ -from unittest import TestCase -from test2text.db.client import DbClient - -class TestDBClient(TestCase): - def test_db_client(self): - db = DbClient(':memory:') - with self.subTest('extensions'): - vec_version, = db.conn.execute("select vec_version()").fetchone() - self.assertIsNotNone(vec_version) - with self.subTest(table='annotations'): - with self.subTest('insert 2 different summaries'): - id1 = db.annotations.insert('Summary 1') - id2 = db.annotations.insert('Summary 2') - self.assertEqual(id1, 1) - self.assertEqual(id2, 2) - with self.subTest('insert 2 same summaries'): - id3 = db.annotations.insert('Summary 1') - id4 = db.annotations.insert('Summary 2') - self.assertIsNone(id3) - self.assertIsNone(id4) - with self.subTest('embedding of different size'): - id5 = db.annotations.insert('Summary 3', [1., 2., 3.]) - self.assertIsNone(id5) - with self.subTest('insert summary with embedding'): - id6 = db.annotations.insert('Summary 3', [1.0] * db.annotations.embedding_size) - self.assertEqual(id6, 6) \ No newline at end of file diff --git a/tests/test_embeddings.py b/tests/test_embeddings.py index 6e090bb..f4aa560 100644 --- a/tests/test_embeddings.py +++ b/tests/test_embeddings.py @@ -1,14 +1,26 @@ from unittest import TestCase import logging -logging.basicConfig(level=logging.DEBUG) -from test2text.embeddings.embed import embed_requirement + +logging.basicConfig(level=logging.WARNING) +SKIP_TESTS = False +try: + from test2text.embeddings.embed import embed_requirement +except ImportError: + SKIP_TESTS = True + + def embed_requirement(requirement): + raise ImportError("Embedding model not available in this environment.") + logger = logging.getLogger() + class TestEmbeddings(TestCase): def test_embed_requirement(self): + if SKIP_TESTS: + self.skipTest("Skipping tests due to missing model in CI environment.") requirement = "The system shall allow users to search for documents." - logger.debug('Start embedding') + logger.debug("Start embedding") embedding = embed_requirement(requirement) - logger.debug('End embedding') - self.assertEqual(embedding.shape, (768,)) \ No newline at end of file + logger.debug("End embedding") + self.assertEqual(embedding.shape, (768,)) diff --git a/tests/test_semver.py b/tests/test_semver.py new file mode 100644 index 0000000..b828e20 --- /dev/null +++ b/tests/test_semver.py @@ -0,0 +1,29 @@ +from test2text.utils.semver import Semver +from unittest import TestCase + + +class TestSemver(TestCase): + def test_initialization(self): + version = Semver("1.2.3") + self.assertEqual(version.major, 1) + self.assertEqual(version.minor, 2) + self.assertEqual(version.patch, 3) + + def test_str(self): + version = Semver("1.2.3") + self.assertEqual(str(version), "1.2.3") + + def test_equality(self): + version1 = Semver("1.2.3") + version2 = Semver("1.2.3") + self.assertTrue(version1 == version2) + self.assertTrue(version1 == "1.2.3") + self.assertFalse(version1 != version2) + + def test_comparison(self): + version1 = Semver("1.2.3") + version2 = Semver("1.2.4") + self.assertTrue(version1 < version2) + self.assertTrue(version1 <= version2) + self.assertTrue(version2 > version1) + self.assertTrue(version2 >= version1) diff --git a/uv.lock b/uv.lock index 4fe2c3e..29b557e 100644 --- a/uv.lock +++ b/uv.lock @@ -251,6 +251,80 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/94/86bfae441707205634d80392e873295652fc313dfd93c233c52c4dc07874/contourpy-1.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:44a29502ca9c7b5ba389e620d44f2fbe792b1fb5734e8b931ad307071ec58c53", size = 218221, upload-time = "2024-11-12T10:58:00.033Z" }, ] +[[package]] +name = "coverage" +version = "7.9.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/b7/c0465ca253df10a9e8dae0692a4ae6e9726d245390aaef92360e1d6d3832/coverage-7.9.2.tar.gz", hash = "sha256:997024fa51e3290264ffd7492ec97d0690293ccd2b45a6cd7d82d945a4a80c8b", size = 813556, upload-time = "2025-07-03T10:54:15.101Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/0d/5c2114fd776c207bd55068ae8dc1bef63ecd1b767b3389984a8e58f2b926/coverage-7.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:66283a192a14a3854b2e7f3418d7db05cdf411012ab7ff5db98ff3b181e1f912", size = 212039, upload-time = "2025-07-03T10:52:38.955Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ad/dc51f40492dc2d5fcd31bb44577bc0cc8920757d6bc5d3e4293146524ef9/coverage-7.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4e01d138540ef34fcf35c1aa24d06c3de2a4cffa349e29a10056544f35cca15f", size = 212428, upload-time = "2025-07-03T10:52:41.36Z" }, + { url = "https://files.pythonhosted.org/packages/a2/a3/55cb3ff1b36f00df04439c3993d8529193cdf165a2467bf1402539070f16/coverage-7.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f22627c1fe2745ee98d3ab87679ca73a97e75ca75eb5faee48660d060875465f", size = 241534, upload-time = "2025-07-03T10:52:42.956Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c9/a8410b91b6be4f6e9c2e9f0dce93749b6b40b751d7065b4410bf89cb654b/coverage-7.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b1c2d8363247b46bd51f393f86c94096e64a1cf6906803fa8d5a9d03784bdbf", size = 239408, upload-time = "2025-07-03T10:52:44.199Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c4/6f3e56d467c612b9070ae71d5d3b114c0b899b5788e1ca3c93068ccb7018/coverage-7.9.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c10c882b114faf82dbd33e876d0cbd5e1d1ebc0d2a74ceef642c6152f3f4d547", size = 240552, upload-time = "2025-07-03T10:52:45.477Z" }, + { url = "https://files.pythonhosted.org/packages/fd/20/04eda789d15af1ce79bce5cc5fd64057c3a0ac08fd0576377a3096c24663/coverage-7.9.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:de3c0378bdf7066c3988d66cd5232d161e933b87103b014ab1b0b4676098fa45", size = 240464, upload-time = "2025-07-03T10:52:46.809Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5a/217b32c94cc1a0b90f253514815332d08ec0812194a1ce9cca97dda1cd20/coverage-7.9.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1e2f097eae0e5991e7623958a24ced3282676c93c013dde41399ff63e230fcf2", size = 239134, upload-time = "2025-07-03T10:52:48.149Z" }, + { url = "https://files.pythonhosted.org/packages/34/73/1d019c48f413465eb5d3b6898b6279e87141c80049f7dbf73fd020138549/coverage-7.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28dc1f67e83a14e7079b6cea4d314bc8b24d1aed42d3582ff89c0295f09b181e", size = 239405, upload-time = "2025-07-03T10:52:49.687Z" }, + { url = "https://files.pythonhosted.org/packages/49/6c/a2beca7aa2595dad0c0d3f350382c381c92400efe5261e2631f734a0e3fe/coverage-7.9.2-cp310-cp310-win32.whl", hash = "sha256:bf7d773da6af9e10dbddacbf4e5cab13d06d0ed93561d44dae0188a42c65be7e", size = 214519, upload-time = "2025-07-03T10:52:51.036Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c8/91e5e4a21f9a51e2c7cdd86e587ae01a4fcff06fc3fa8cde4d6f7cf68df4/coverage-7.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:0c0378ba787681ab1897f7c89b415bd56b0b2d9a47e5a3d8dc0ea55aac118d6c", size = 215400, upload-time = "2025-07-03T10:52:52.313Z" }, + { url = "https://files.pythonhosted.org/packages/39/40/916786453bcfafa4c788abee4ccd6f592b5b5eca0cd61a32a4e5a7ef6e02/coverage-7.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a7a56a2964a9687b6aba5b5ced6971af308ef6f79a91043c05dd4ee3ebc3e9ba", size = 212152, upload-time = "2025-07-03T10:52:53.562Z" }, + { url = "https://files.pythonhosted.org/packages/9f/66/cc13bae303284b546a030762957322bbbff1ee6b6cb8dc70a40f8a78512f/coverage-7.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123d589f32c11d9be7fe2e66d823a236fe759b0096f5db3fb1b75b2fa414a4fa", size = 212540, upload-time = "2025-07-03T10:52:55.196Z" }, + { url = "https://files.pythonhosted.org/packages/0f/3c/d56a764b2e5a3d43257c36af4a62c379df44636817bb5f89265de4bf8bd7/coverage-7.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:333b2e0ca576a7dbd66e85ab402e35c03b0b22f525eed82681c4b866e2e2653a", size = 245097, upload-time = "2025-07-03T10:52:56.509Z" }, + { url = "https://files.pythonhosted.org/packages/b1/46/bd064ea8b3c94eb4ca5d90e34d15b806cba091ffb2b8e89a0d7066c45791/coverage-7.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:326802760da234baf9f2f85a39e4a4b5861b94f6c8d95251f699e4f73b1835dc", size = 242812, upload-time = "2025-07-03T10:52:57.842Z" }, + { url = "https://files.pythonhosted.org/packages/43/02/d91992c2b29bc7afb729463bc918ebe5f361be7f1daae93375a5759d1e28/coverage-7.9.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19e7be4cfec248df38ce40968c95d3952fbffd57b400d4b9bb580f28179556d2", size = 244617, upload-time = "2025-07-03T10:52:59.239Z" }, + { url = "https://files.pythonhosted.org/packages/b7/4f/8fadff6bf56595a16d2d6e33415841b0163ac660873ed9a4e9046194f779/coverage-7.9.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0b4a4cb73b9f2b891c1788711408ef9707666501ba23684387277ededab1097c", size = 244263, upload-time = "2025-07-03T10:53:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d2/e0be7446a2bba11739edb9f9ba4eff30b30d8257370e237418eb44a14d11/coverage-7.9.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2c8937fa16c8c9fbbd9f118588756e7bcdc7e16a470766a9aef912dd3f117dbd", size = 242314, upload-time = "2025-07-03T10:53:01.932Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7d/dcbac9345000121b8b57a3094c2dfcf1ccc52d8a14a40c1d4bc89f936f80/coverage-7.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:42da2280c4d30c57a9b578bafd1d4494fa6c056d4c419d9689e66d775539be74", size = 242904, upload-time = "2025-07-03T10:53:03.478Z" }, + { url = "https://files.pythonhosted.org/packages/41/58/11e8db0a0c0510cf31bbbdc8caf5d74a358b696302a45948d7c768dfd1cf/coverage-7.9.2-cp311-cp311-win32.whl", hash = "sha256:14fa8d3da147f5fdf9d298cacc18791818f3f1a9f542c8958b80c228320e90c6", size = 214553, upload-time = "2025-07-03T10:53:05.174Z" }, + { url = "https://files.pythonhosted.org/packages/3a/7d/751794ec8907a15e257136e48dc1021b1f671220ecccfd6c4eaf30802714/coverage-7.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:549cab4892fc82004f9739963163fd3aac7a7b0df430669b75b86d293d2df2a7", size = 215441, upload-time = "2025-07-03T10:53:06.472Z" }, + { url = "https://files.pythonhosted.org/packages/62/5b/34abcedf7b946c1c9e15b44f326cb5b0da852885312b30e916f674913428/coverage-7.9.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2667a2b913e307f06aa4e5677f01a9746cd08e4b35e14ebcde6420a9ebb4c62", size = 213873, upload-time = "2025-07-03T10:53:07.699Z" }, + { url = "https://files.pythonhosted.org/packages/53/d7/7deefc6fd4f0f1d4c58051f4004e366afc9e7ab60217ac393f247a1de70a/coverage-7.9.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae9eb07f1cfacd9cfe8eaee6f4ff4b8a289a668c39c165cd0c8548484920ffc0", size = 212344, upload-time = "2025-07-03T10:53:09.3Z" }, + { url = "https://files.pythonhosted.org/packages/95/0c/ee03c95d32be4d519e6a02e601267769ce2e9a91fc8faa1b540e3626c680/coverage-7.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ce85551f9a1119f02adc46d3014b5ee3f765deac166acf20dbb851ceb79b6f3", size = 212580, upload-time = "2025-07-03T10:53:11.52Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9f/826fa4b544b27620086211b87a52ca67592622e1f3af9e0a62c87aea153a/coverage-7.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8f6389ac977c5fb322e0e38885fbbf901743f79d47f50db706e7644dcdcb6e1", size = 246383, upload-time = "2025-07-03T10:53:13.134Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b3/4477aafe2a546427b58b9c540665feff874f4db651f4d3cb21b308b3a6d2/coverage-7.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff0d9eae8cdfcd58fe7893b88993723583a6ce4dfbfd9f29e001922544f95615", size = 243400, upload-time = "2025-07-03T10:53:14.614Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c2/efffa43778490c226d9d434827702f2dfbc8041d79101a795f11cbb2cf1e/coverage-7.9.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae939811e14e53ed8a9818dad51d434a41ee09df9305663735f2e2d2d7d959b", size = 245591, upload-time = "2025-07-03T10:53:15.872Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e7/a59888e882c9a5f0192d8627a30ae57910d5d449c80229b55e7643c078c4/coverage-7.9.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:31991156251ec202c798501e0a42bbdf2169dcb0f137b1f5c0f4267f3fc68ef9", size = 245402, upload-time = "2025-07-03T10:53:17.124Z" }, + { url = "https://files.pythonhosted.org/packages/92/a5/72fcd653ae3d214927edc100ce67440ed8a0a1e3576b8d5e6d066ed239db/coverage-7.9.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d0d67963f9cbfc7c7f96d4ac74ed60ecbebd2ea6eeb51887af0f8dce205e545f", size = 243583, upload-time = "2025-07-03T10:53:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/5c/f5/84e70e4df28f4a131d580d7d510aa1ffd95037293da66fd20d446090a13b/coverage-7.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49b752a2858b10580969ec6af6f090a9a440a64a301ac1528d7ca5f7ed497f4d", size = 244815, upload-time = "2025-07-03T10:53:20.168Z" }, + { url = "https://files.pythonhosted.org/packages/39/e7/d73d7cbdbd09fdcf4642655ae843ad403d9cbda55d725721965f3580a314/coverage-7.9.2-cp312-cp312-win32.whl", hash = "sha256:88d7598b8ee130f32f8a43198ee02edd16d7f77692fa056cb779616bbea1b355", size = 214719, upload-time = "2025-07-03T10:53:21.521Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d6/7486dcc3474e2e6ad26a2af2db7e7c162ccd889c4c68fa14ea8ec189c9e9/coverage-7.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:9dfb070f830739ee49d7c83e4941cc767e503e4394fdecb3b54bfdac1d7662c0", size = 215509, upload-time = "2025-07-03T10:53:22.853Z" }, + { url = "https://files.pythonhosted.org/packages/b7/34/0439f1ae2593b0346164d907cdf96a529b40b7721a45fdcf8b03c95fcd90/coverage-7.9.2-cp312-cp312-win_arm64.whl", hash = "sha256:4e2c058aef613e79df00e86b6d42a641c877211384ce5bd07585ed7ba71ab31b", size = 213910, upload-time = "2025-07-03T10:53:24.472Z" }, + { url = "https://files.pythonhosted.org/packages/94/9d/7a8edf7acbcaa5e5c489a646226bed9591ee1c5e6a84733c0140e9ce1ae1/coverage-7.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:985abe7f242e0d7bba228ab01070fde1d6c8fa12f142e43debe9ed1dde686038", size = 212367, upload-time = "2025-07-03T10:53:25.811Z" }, + { url = "https://files.pythonhosted.org/packages/e8/9e/5cd6f130150712301f7e40fb5865c1bc27b97689ec57297e568d972eec3c/coverage-7.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c3939264a76d44fde7f213924021ed31f55ef28111a19649fec90c0f109e6d", size = 212632, upload-time = "2025-07-03T10:53:27.075Z" }, + { url = "https://files.pythonhosted.org/packages/a8/de/6287a2c2036f9fd991c61cefa8c64e57390e30c894ad3aa52fac4c1e14a8/coverage-7.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae5d563e970dbe04382f736ec214ef48103d1b875967c89d83c6e3f21706d5b3", size = 245793, upload-time = "2025-07-03T10:53:28.408Z" }, + { url = "https://files.pythonhosted.org/packages/06/cc/9b5a9961d8160e3cb0b558c71f8051fe08aa2dd4b502ee937225da564ed1/coverage-7.9.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdd612e59baed2a93c8843c9a7cb902260f181370f1d772f4842987535071d14", size = 243006, upload-time = "2025-07-03T10:53:29.754Z" }, + { url = "https://files.pythonhosted.org/packages/49/d9/4616b787d9f597d6443f5588619c1c9f659e1f5fc9eebf63699eb6d34b78/coverage-7.9.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:256ea87cb2a1ed992bcdfc349d8042dcea1b80436f4ddf6e246d6bee4b5d73b6", size = 244990, upload-time = "2025-07-03T10:53:31.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/83/801cdc10f137b2d02b005a761661649ffa60eb173dcdaeb77f571e4dc192/coverage-7.9.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f44ae036b63c8ea432f610534a2668b0c3aee810e7037ab9d8ff6883de480f5b", size = 245157, upload-time = "2025-07-03T10:53:32.717Z" }, + { url = "https://files.pythonhosted.org/packages/c8/a4/41911ed7e9d3ceb0ffb019e7635468df7499f5cc3edca5f7dfc078e9c5ec/coverage-7.9.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82d76ad87c932935417a19b10cfe7abb15fd3f923cfe47dbdaa74ef4e503752d", size = 243128, upload-time = "2025-07-03T10:53:34.009Z" }, + { url = "https://files.pythonhosted.org/packages/10/41/344543b71d31ac9cb00a664d5d0c9ef134a0fe87cb7d8430003b20fa0b7d/coverage-7.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:619317bb86de4193debc712b9e59d5cffd91dc1d178627ab2a77b9870deb2868", size = 244511, upload-time = "2025-07-03T10:53:35.434Z" }, + { url = "https://files.pythonhosted.org/packages/d5/81/3b68c77e4812105e2a060f6946ba9e6f898ddcdc0d2bfc8b4b152a9ae522/coverage-7.9.2-cp313-cp313-win32.whl", hash = "sha256:0a07757de9feb1dfafd16ab651e0f628fd7ce551604d1bf23e47e1ddca93f08a", size = 214765, upload-time = "2025-07-03T10:53:36.787Z" }, + { url = "https://files.pythonhosted.org/packages/06/a2/7fac400f6a346bb1a4004eb2a76fbff0e242cd48926a2ce37a22a6a1d917/coverage-7.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:115db3d1f4d3f35f5bb021e270edd85011934ff97c8797216b62f461dd69374b", size = 215536, upload-time = "2025-07-03T10:53:38.188Z" }, + { url = "https://files.pythonhosted.org/packages/08/47/2c6c215452b4f90d87017e61ea0fd9e0486bb734cb515e3de56e2c32075f/coverage-7.9.2-cp313-cp313-win_arm64.whl", hash = "sha256:48f82f889c80af8b2a7bb6e158d95a3fbec6a3453a1004d04e4f3b5945a02694", size = 213943, upload-time = "2025-07-03T10:53:39.492Z" }, + { url = "https://files.pythonhosted.org/packages/a3/46/e211e942b22d6af5e0f323faa8a9bc7c447a1cf1923b64c47523f36ed488/coverage-7.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:55a28954545f9d2f96870b40f6c3386a59ba8ed50caf2d949676dac3ecab99f5", size = 213088, upload-time = "2025-07-03T10:53:40.874Z" }, + { url = "https://files.pythonhosted.org/packages/d2/2f/762551f97e124442eccd907bf8b0de54348635b8866a73567eb4e6417acf/coverage-7.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cdef6504637731a63c133bb2e6f0f0214e2748495ec15fe42d1e219d1b133f0b", size = 213298, upload-time = "2025-07-03T10:53:42.218Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b7/76d2d132b7baf7360ed69be0bcab968f151fa31abe6d067f0384439d9edb/coverage-7.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcd5ebe66c7a97273d5d2ddd4ad0ed2e706b39630ed4b53e713d360626c3dbb3", size = 256541, upload-time = "2025-07-03T10:53:43.823Z" }, + { url = "https://files.pythonhosted.org/packages/a0/17/392b219837d7ad47d8e5974ce5f8dc3deb9f99a53b3bd4d123602f960c81/coverage-7.9.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9303aed20872d7a3c9cb39c5d2b9bdbe44e3a9a1aecb52920f7e7495410dfab8", size = 252761, upload-time = "2025-07-03T10:53:45.19Z" }, + { url = "https://files.pythonhosted.org/packages/d5/77/4256d3577fe1b0daa8d3836a1ebe68eaa07dd2cbaf20cf5ab1115d6949d4/coverage-7.9.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc18ea9e417a04d1920a9a76fe9ebd2f43ca505b81994598482f938d5c315f46", size = 254917, upload-time = "2025-07-03T10:53:46.931Z" }, + { url = "https://files.pythonhosted.org/packages/53/99/fc1a008eef1805e1ddb123cf17af864743354479ea5129a8f838c433cc2c/coverage-7.9.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6406cff19880aaaadc932152242523e892faff224da29e241ce2fca329866584", size = 256147, upload-time = "2025-07-03T10:53:48.289Z" }, + { url = "https://files.pythonhosted.org/packages/92/c0/f63bf667e18b7f88c2bdb3160870e277c4874ced87e21426128d70aa741f/coverage-7.9.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d0d4f6ecdf37fcc19c88fec3e2277d5dee740fb51ffdd69b9579b8c31e4232e", size = 254261, upload-time = "2025-07-03T10:53:49.99Z" }, + { url = "https://files.pythonhosted.org/packages/8c/32/37dd1c42ce3016ff8ec9e4b607650d2e34845c0585d3518b2a93b4830c1a/coverage-7.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c33624f50cf8de418ab2b4d6ca9eda96dc45b2c4231336bac91454520e8d1fac", size = 255099, upload-time = "2025-07-03T10:53:51.354Z" }, + { url = "https://files.pythonhosted.org/packages/da/2e/af6b86f7c95441ce82f035b3affe1cd147f727bbd92f563be35e2d585683/coverage-7.9.2-cp313-cp313t-win32.whl", hash = "sha256:1df6b76e737c6a92210eebcb2390af59a141f9e9430210595251fbaf02d46926", size = 215440, upload-time = "2025-07-03T10:53:52.808Z" }, + { url = "https://files.pythonhosted.org/packages/4d/bb/8a785d91b308867f6b2e36e41c569b367c00b70c17f54b13ac29bcd2d8c8/coverage-7.9.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f5fd54310b92741ebe00d9c0d1d7b2b27463952c022da6d47c175d246a98d1bd", size = 216537, upload-time = "2025-07-03T10:53:54.273Z" }, + { url = "https://files.pythonhosted.org/packages/1d/a0/a6bffb5e0f41a47279fd45a8f3155bf193f77990ae1c30f9c224b61cacb0/coverage-7.9.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c48c2375287108c887ee87d13b4070a381c6537d30e8487b24ec721bf2a781cb", size = 214398, upload-time = "2025-07-03T10:53:56.715Z" }, + { url = "https://files.pythonhosted.org/packages/62/ab/b4b06662ccaa00ca7bbee967b7035a33a58b41efb92d8c89a6c523a2ccd5/coverage-7.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ddc39510ac922a5c4c27849b739f875d3e1d9e590d1e7b64c98dadf037a16cce", size = 212037, upload-time = "2025-07-03T10:53:58.055Z" }, + { url = "https://files.pythonhosted.org/packages/bb/5e/04619995657acc898d15bfad42b510344b3a74d4d5bc34f2e279d46c781c/coverage-7.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a535c0c7364acd55229749c2b3e5eebf141865de3a8f697076a3291985f02d30", size = 212412, upload-time = "2025-07-03T10:53:59.451Z" }, + { url = "https://files.pythonhosted.org/packages/14/e7/1465710224dc6d31c534e7714cbd907210622a044adc81c810e72eea873f/coverage-7.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df0f9ef28e0f20c767ccdccfc5ae5f83a6f4a2fbdfbcbcc8487a8a78771168c8", size = 241164, upload-time = "2025-07-03T10:54:00.852Z" }, + { url = "https://files.pythonhosted.org/packages/ab/f2/44c6fbd2794afeb9ab6c0a14d3c088ab1dae3dff3df2624609981237bbb4/coverage-7.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f3da12e0ccbcb348969221d29441ac714bbddc4d74e13923d3d5a7a0bebef7a", size = 239032, upload-time = "2025-07-03T10:54:02.25Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d2/7a79845429c0aa2e6788bc45c26a2e3052fa91082c9ea1dea56fb531952c/coverage-7.9.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a17eaf46f56ae0f870f14a3cbc2e4632fe3771eab7f687eda1ee59b73d09fe4", size = 240148, upload-time = "2025-07-03T10:54:03.618Z" }, + { url = "https://files.pythonhosted.org/packages/9c/7d/2731d1b4c9c672d82d30d218224dfc62939cf3800bc8aba0258fefb191f5/coverage-7.9.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:669135a9d25df55d1ed56a11bf555f37c922cf08d80799d4f65d77d7d6123fcf", size = 239875, upload-time = "2025-07-03T10:54:05.022Z" }, + { url = "https://files.pythonhosted.org/packages/1b/83/685958715429a9da09cf172c15750ca5c795dd7259466f2645403696557b/coverage-7.9.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9d3a700304d01a627df9db4322dc082a0ce1e8fc74ac238e2af39ced4c083193", size = 238127, upload-time = "2025-07-03T10:54:06.366Z" }, + { url = "https://files.pythonhosted.org/packages/34/ff/161a4313308b3783126790adfae1970adbe4886fda8788792e435249910a/coverage-7.9.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:71ae8b53855644a0b1579d4041304ddc9995c7b21c8a1f16753c4d8903b4dfed", size = 239064, upload-time = "2025-07-03T10:54:07.878Z" }, + { url = "https://files.pythonhosted.org/packages/17/14/fe33f41b2e80811021de059621f44c01ebe4d6b08bdb82d54a514488e933/coverage-7.9.2-cp39-cp39-win32.whl", hash = "sha256:dd7a57b33b5cf27acb491e890720af45db05589a80c1ffc798462a765be6d4d7", size = 214522, upload-time = "2025-07-03T10:54:09.331Z" }, + { url = "https://files.pythonhosted.org/packages/6e/30/63d850ec31b5c6f6a7b4e853016375b846258300320eda29376e2786ceeb/coverage-7.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:f65bb452e579d5540c8b37ec105dd54d8b9307b07bcaa186818c104ffda22441", size = 215419, upload-time = "2025-07-03T10:54:10.681Z" }, + { url = "https://files.pythonhosted.org/packages/d7/85/f8bbefac27d286386961c25515431482a425967e23d3698b75a250872924/coverage-7.9.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:8a1166db2fb62473285bcb092f586e081e92656c7dfa8e9f62b4d39d7e6b5050", size = 204013, upload-time = "2025-07-03T10:54:12.084Z" }, + { url = "https://files.pythonhosted.org/packages/3c/38/bbe2e63902847cf79036ecc75550d0698af31c91c7575352eb25190d0fb3/coverage-7.9.2-py3-none-any.whl", hash = "sha256:e425cd5b00f6fc0ed7cdbd766c70be8baab4b7839e4d4fe5fac48581dd968ea4", size = 204005, upload-time = "2025-07-03T10:54:13.491Z" }, +] + [[package]] name = "cycler" version = "0.12.1" @@ -1205,6 +1279,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" }, ] +[[package]] +name = "ruff" +version = "0.12.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/2a/43955b530c49684d3c38fcda18c43caf91e99204c2a065552528e0552d4f/ruff-0.12.3.tar.gz", hash = "sha256:f1b5a4b6668fd7b7ea3697d8d98857390b40c1320a63a178eee6be0899ea2d77", size = 4459341, upload-time = "2025-07-11T13:21:16.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/fd/b44c5115539de0d598d75232a1cc7201430b6891808df111b8b0506aae43/ruff-0.12.3-py3-none-linux_armv6l.whl", hash = "sha256:47552138f7206454eaf0c4fe827e546e9ddac62c2a3d2585ca54d29a890137a2", size = 10430499, upload-time = "2025-07-11T13:20:26.321Z" }, + { url = "https://files.pythonhosted.org/packages/43/c5/9eba4f337970d7f639a37077be067e4ec80a2ad359e4cc6c5b56805cbc66/ruff-0.12.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0a9153b000c6fe169bb307f5bd1b691221c4286c133407b8827c406a55282041", size = 11213413, upload-time = "2025-07-11T13:20:30.017Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2c/fac3016236cf1fe0bdc8e5de4f24c76ce53c6dd9b5f350d902549b7719b2/ruff-0.12.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:fa6b24600cf3b750e48ddb6057e901dd5b9aa426e316addb2a1af185a7509882", size = 10586941, upload-time = "2025-07-11T13:20:33.046Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0f/41fec224e9dfa49a139f0b402ad6f5d53696ba1800e0f77b279d55210ca9/ruff-0.12.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2506961bf6ead54887ba3562604d69cb430f59b42133d36976421bc8bd45901", size = 10783001, upload-time = "2025-07-11T13:20:35.534Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ca/dd64a9ce56d9ed6cad109606ac014860b1c217c883e93bf61536400ba107/ruff-0.12.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c4faaff1f90cea9d3033cbbcdf1acf5d7fb11d8180758feb31337391691f3df0", size = 10269641, upload-time = "2025-07-11T13:20:38.459Z" }, + { url = "https://files.pythonhosted.org/packages/63/5c/2be545034c6bd5ce5bb740ced3e7014d7916f4c445974be11d2a406d5088/ruff-0.12.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40dced4a79d7c264389de1c59467d5d5cefd79e7e06d1dfa2c75497b5269a5a6", size = 11875059, upload-time = "2025-07-11T13:20:41.517Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d4/a74ef1e801ceb5855e9527dae105eaff136afcb9cc4d2056d44feb0e4792/ruff-0.12.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:0262d50ba2767ed0fe212aa7e62112a1dcbfd46b858c5bf7bbd11f326998bafc", size = 12658890, upload-time = "2025-07-11T13:20:44.442Z" }, + { url = "https://files.pythonhosted.org/packages/13/c8/1057916416de02e6d7c9bcd550868a49b72df94e3cca0aeb77457dcd9644/ruff-0.12.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12371aec33e1a3758597c5c631bae9a5286f3c963bdfb4d17acdd2d395406687", size = 12232008, upload-time = "2025-07-11T13:20:47.374Z" }, + { url = "https://files.pythonhosted.org/packages/f5/59/4f7c130cc25220392051fadfe15f63ed70001487eca21d1796db46cbcc04/ruff-0.12.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:560f13b6baa49785665276c963edc363f8ad4b4fc910a883e2625bdb14a83a9e", size = 11499096, upload-time = "2025-07-11T13:20:50.348Z" }, + { url = "https://files.pythonhosted.org/packages/d4/01/a0ad24a5d2ed6be03a312e30d32d4e3904bfdbc1cdbe63c47be9d0e82c79/ruff-0.12.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:023040a3499f6f974ae9091bcdd0385dd9e9eb4942f231c23c57708147b06311", size = 11688307, upload-time = "2025-07-11T13:20:52.945Z" }, + { url = "https://files.pythonhosted.org/packages/93/72/08f9e826085b1f57c9a0226e48acb27643ff19b61516a34c6cab9d6ff3fa/ruff-0.12.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:883d844967bffff5ab28bba1a4d246c1a1b2933f48cb9840f3fdc5111c603b07", size = 10661020, upload-time = "2025-07-11T13:20:55.799Z" }, + { url = "https://files.pythonhosted.org/packages/80/a0/68da1250d12893466c78e54b4a0ff381370a33d848804bb51279367fc688/ruff-0.12.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:2120d3aa855ff385e0e562fdee14d564c9675edbe41625c87eeab744a7830d12", size = 10246300, upload-time = "2025-07-11T13:20:58.222Z" }, + { url = "https://files.pythonhosted.org/packages/6a/22/5f0093d556403e04b6fd0984fc0fb32fbb6f6ce116828fd54306a946f444/ruff-0.12.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6b16647cbb470eaf4750d27dddc6ebf7758b918887b56d39e9c22cce2049082b", size = 11263119, upload-time = "2025-07-11T13:21:01.503Z" }, + { url = "https://files.pythonhosted.org/packages/92/c9/f4c0b69bdaffb9968ba40dd5fa7df354ae0c73d01f988601d8fac0c639b1/ruff-0.12.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e1417051edb436230023575b149e8ff843a324557fe0a265863b7602df86722f", size = 11746990, upload-time = "2025-07-11T13:21:04.524Z" }, + { url = "https://files.pythonhosted.org/packages/fe/84/7cc7bd73924ee6be4724be0db5414a4a2ed82d06b30827342315a1be9e9c/ruff-0.12.3-py3-none-win32.whl", hash = "sha256:dfd45e6e926deb6409d0616078a666ebce93e55e07f0fb0228d4b2608b2c248d", size = 10589263, upload-time = "2025-07-11T13:21:07.148Z" }, + { url = "https://files.pythonhosted.org/packages/07/87/c070f5f027bd81f3efee7d14cb4d84067ecf67a3a8efb43aadfc72aa79a6/ruff-0.12.3-py3-none-win_amd64.whl", hash = "sha256:a946cf1e7ba3209bdef039eb97647f1c77f6f540e5845ec9c114d3af8df873e7", size = 11695072, upload-time = "2025-07-11T13:21:11.004Z" }, + { url = "https://files.pythonhosted.org/packages/e0/30/f3eaf6563c637b6e66238ed6535f6775480db973c836336e4122161986fc/ruff-0.12.3-py3-none-win_arm64.whl", hash = "sha256:5f9c7c9c8f84c2d7f27e93674d27136fbf489720251544c4da7fb3d742e011b1", size = 10805855, upload-time = "2025-07-11T13:21:13.547Z" }, +] + [[package]] name = "safetensors" version = "0.5.3" @@ -1453,23 +1552,39 @@ name = "test2text" version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "einops" }, { name = "matplotlib", version = "3.9.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, { name = "matplotlib", version = "3.10.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.10'" }, - { name = "sentence-transformers" }, { name = "sqlite-vec" }, { name = "tabbyset" }, +] + +[package.dev-dependencies] +dev = [ + { name = "coverage" }, + { name = "ruff" }, +] +production = [ + { name = "einops" }, + { name = "sentence-transformers" }, { name = "torch", version = "2.6.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, { name = "torch", version = "2.6.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" }, ] [package.metadata] requires-dist = [ - { name = "einops", specifier = ">=0.8.1" }, { name = "matplotlib", specifier = ">=3.9.4" }, - { name = "sentence-transformers", specifier = ">=4.0.1" }, { name = "sqlite-vec", specifier = ">=0.1.6" }, { name = "tabbyset", specifier = ">=1.0.0" }, +] + +[package.metadata.requires-dev] +dev = [ + { name = "coverage", specifier = ">=7.9.2" }, + { name = "ruff", specifier = ">=0.12.3" }, +] +production = [ + { name = "einops", specifier = ">=0.8.1" }, + { name = "sentence-transformers", specifier = ">=4.0.1" }, { name = "torch", index = "https://download.pytorch.org/whl/cpu" }, ] diff --git a/visualize_vectors.py b/visualize_vectors.py index 4cec79a..71874c1 100644 --- a/visualize_vectors.py +++ b/visualize_vectors.py @@ -1,6 +1,7 @@ import numpy as np import matplotlib.pyplot as plt import logging + logging.basicConfig(level=logging.DEBUG) from sklearn.manifold import TSNE from test2text.utils.sqlite_vec import unpack_float32 @@ -11,12 +12,14 @@ DOT_SIZE_2D = 20 DOT_SIZE_3D = 10 + def extract_annotation_vectors(db: DbClient): vectors = [] for row in db.conn.execute("SELECT embedding FROM Annotations").fetchall(): vectors.append(np.array(unpack_float32(row[0]))) return np.array(vectors) + def extract_closest_annotation_vectors(db: DbClient): vectors = [] for row in db.conn.execute(""" @@ -28,87 +31,133 @@ def extract_closest_annotation_vectors(db: DbClient): vectors.append(np.array(unpack_float32(row[0]))) return np.array(vectors) + def extract_requirement_vectors(db: DbClient): vectors = [] for row in db.conn.execute("SELECT embedding FROM Requirements").fetchall(): vectors.append(np.array(unpack_float32(row[0]))) return np.array(vectors) + def minifold_vectors_2d(vectors: np.array): tsne = TSNE(n_components=2, random_state=0) vectors_2d = tsne.fit_transform(vectors) return vectors_2d + def minifold_vectors_3d(vectors: np.array): tsne = TSNE(n_components=3, random_state=0) vectors_3d = tsne.fit_transform(vectors) return vectors_3d + def plot_vectors_2d(vectors_2d: np.array, title: str): plt.figure(figsize=FIG_SIZE) plt.scatter(vectors_2d[:, 0], vectors_2d[:, 1], alpha=0.7, s=DOT_SIZE_2D) plt.title(title) plt.grid(True) - plt.savefig(f'./private/{title} vectors 2d.png') + plt.savefig(f"./private/{title} vectors 2d.png") + def plot_vectors_3d(vectors_3d: np.array, title: str): fig = plt.figure(figsize=FIG_SIZE) - ax = fig.add_subplot(111, projection='3d') - ax.scatter(vectors_3d[:, 0], vectors_3d[:, 1], vectors_3d[:, 2], alpha=0.7, s=DOT_SIZE_3D) + ax = fig.add_subplot(111, projection="3d") + ax.scatter( + vectors_3d[:, 0], vectors_3d[:, 1], vectors_3d[:, 2], alpha=0.7, s=DOT_SIZE_3D + ) plt.title(title) plt.grid(True) - plt.savefig(f'./private/{title} vectors 3d.png') + plt.savefig(f"./private/{title} vectors 3d.png") + if __name__ == "__main__": - logging.info('Visualizing vectors') + logging.info("Visualizing vectors") db = DbClient("./private/requirements.db") - logging.info('Database connected') + logging.info("Database connected") requirement_vectors = extract_requirement_vectors(db) - logging.info('Requirement vectors extracted') + logging.info("Requirement vectors extracted") reqs_vectors_2d = minifold_vectors_2d(requirement_vectors) - logging.info('Requirement vectors 2d minifolded') - plot_vectors_2d(reqs_vectors_2d, 'Requirements') - logging.info('Requirement vectors 2d plotted') + logging.info("Requirement vectors 2d minifolded") + plot_vectors_2d(reqs_vectors_2d, "Requirements") + logging.info("Requirement vectors 2d plotted") reqs_vectors_3d = minifold_vectors_3d(requirement_vectors) - logging.info('Requirement vectors 3d minifolded') - plot_vectors_3d(reqs_vectors_3d, 'Requirements') - logging.info('Requirement vectors 3d plotted') + logging.info("Requirement vectors 3d minifolded") + plot_vectors_3d(reqs_vectors_3d, "Requirements") + logging.info("Requirement vectors 3d plotted") annotation_vectors = extract_annotation_vectors(db) - logging.info('Annotation vectors extracted') + logging.info("Annotation vectors extracted") anno_vectors_2d = minifold_vectors_2d(annotation_vectors) - logging.info('Annotation vectors 2d minifolded') - plot_vectors_2d(anno_vectors_2d, 'Annotations') - logging.info('Annotation vectors 2d plotted') + logging.info("Annotation vectors 2d minifolded") + plot_vectors_2d(anno_vectors_2d, "Annotations") + logging.info("Annotation vectors 2d plotted") anno_vectors_3d = minifold_vectors_3d(annotation_vectors) - logging.info('Annotation vectors 3d minifolded') - plot_vectors_3d(anno_vectors_3d, 'Annotations') - logging.info('Annotation vectors 3d plotted') + logging.info("Annotation vectors 3d minifolded") + plot_vectors_3d(anno_vectors_3d, "Annotations") + logging.info("Annotation vectors 3d plotted") # Show how these 2 groups of vectors are different plt.figure(figsize=FIG_SIZE) - plt.scatter(reqs_vectors_2d[:, 0], reqs_vectors_2d[:, 1], alpha=0.5, s=DOT_SIZE_2D, label='Requirements') - plt.scatter(anno_vectors_2d[:, 0], anno_vectors_2d[:, 1], alpha=0.5, s=DOT_SIZE_2D, label='Annotations') - plt.title('Requirements vs Annotations') + plt.scatter( + reqs_vectors_2d[:, 0], + reqs_vectors_2d[:, 1], + alpha=0.5, + s=DOT_SIZE_2D, + label="Requirements", + ) + plt.scatter( + anno_vectors_2d[:, 0], + anno_vectors_2d[:, 1], + alpha=0.5, + s=DOT_SIZE_2D, + label="Annotations", + ) + plt.title("Requirements vs Annotations") plt.legend(fontsize=FONT_SIZE) plt.grid(True) - plt.savefig(f'./private/Requirements vs Annotations vectors 2d.png') + plt.savefig("./private/Requirements vs Annotations vectors 2d.png") fig = plt.figure(figsize=FIG_SIZE) - ax = fig.add_subplot(111, projection='3d') - ax.scatter(reqs_vectors_3d[:, 0], reqs_vectors_3d[:, 1], reqs_vectors_3d[:, 2], alpha=0.5, s=DOT_SIZE_3D, label='Requirements') - ax.scatter(anno_vectors_3d[:, 0], anno_vectors_3d[:, 1], anno_vectors_3d[:, 2], alpha=0.5, s=DOT_SIZE_3D, label='Annotations') - plt.title('Requirements vs Annotations') + ax = fig.add_subplot(111, projection="3d") + ax.scatter( + reqs_vectors_3d[:, 0], + reqs_vectors_3d[:, 1], + reqs_vectors_3d[:, 2], + alpha=0.5, + s=DOT_SIZE_3D, + label="Requirements", + ) + ax.scatter( + anno_vectors_3d[:, 0], + anno_vectors_3d[:, 1], + anno_vectors_3d[:, 2], + alpha=0.5, + s=DOT_SIZE_3D, + label="Annotations", + ) + plt.title("Requirements vs Annotations") plt.legend(fontsize=FONT_SIZE) plt.grid(True) - plt.savefig(f'./private/Requirements vs Annotations vectors 3d.png') + plt.savefig("./private/Requirements vs Annotations vectors 3d.png") anno_vectors_2d = minifold_vectors_2d(extract_closest_annotation_vectors(db)) plt.figure(figsize=FIG_SIZE) - plt.scatter(reqs_vectors_2d[:, 0], reqs_vectors_2d[:, 1], alpha=0.5, s=40, label='Requirements') - plt.scatter(anno_vectors_2d[:, 0], anno_vectors_2d[:, 1], alpha=0.5, s=40, label='Annotations') - plt.title('Requirements vs Annotations') + plt.scatter( + reqs_vectors_2d[:, 0], + reqs_vectors_2d[:, 1], + alpha=0.5, + s=40, + label="Requirements", + ) + plt.scatter( + anno_vectors_2d[:, 0], + anno_vectors_2d[:, 1], + alpha=0.5, + s=40, + label="Annotations", + ) + plt.title("Requirements vs Annotations") plt.legend(fontsize=FONT_SIZE) plt.grid(True) - plt.savefig(f'./private/Requirements vs Closest Annotations vectors 2d.png') \ No newline at end of file + plt.savefig("./private/Requirements vs Closest Annotations vectors 2d.png")