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.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")