updated series_index to float and added UniqueConstraint to audiobook and audiobook_author
treefmt / nix fmt (pull_request) Failing after 6s
pytest / pytest (pull_request) Successful in 30s
build_systems / build-brain (pull_request) Successful in 47s
build_systems / build-bob (pull_request) Successful in 48s
build_systems / build-leviathan (pull_request) Successful in 55s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m1s
build_systems / build-jeeves (pull_request) Successful in 2m37s
treefmt / nix fmt (pull_request) Failing after 6s
pytest / pytest (pull_request) Successful in 30s
build_systems / build-brain (pull_request) Successful in 47s
build_systems / build-bob (pull_request) Successful in 48s
build_systems / build-leviathan (pull_request) Successful in 55s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m1s
build_systems / build-jeeves (pull_request) Successful in 2m37s
This commit is contained in:
@@ -144,7 +144,7 @@ class CatalogToolRegistry:
|
||||
"title": {"type": "string"},
|
||||
"author_id": {"type": "integer"},
|
||||
"series_id": {"type": ["integer", "null"]},
|
||||
"series_index": {"type": "integer"},
|
||||
"series_index": {"type": "number", "multipleOf": 0.5},
|
||||
},
|
||||
"required": ["title", "author_id", "series_id", "series_index"],
|
||||
},
|
||||
@@ -306,12 +306,7 @@ class CatalogToolRegistry:
|
||||
author_id = required_int(arguments, "author_id")
|
||||
validate_catalog_slug(name, "series")
|
||||
author = self.required_author(author_id)
|
||||
series = self.session.scalar(
|
||||
select(AudiobookSeries).where(
|
||||
AudiobookSeries.name == name,
|
||||
AudiobookSeries.author_id == author.id,
|
||||
),
|
||||
)
|
||||
series = self.find_series_by_catalog_slug(name, author.id)
|
||||
action = "existing"
|
||||
if series is None:
|
||||
series = AudiobookSeries(name=name, author=author)
|
||||
@@ -329,7 +324,7 @@ class CatalogToolRegistry:
|
||||
title = required_string(arguments, "title")
|
||||
author_id = required_int(arguments, "author_id")
|
||||
series_id = optional_int(arguments.get("series_id"), "series_id")
|
||||
series_index = required_int(arguments, "series_index")
|
||||
series_index = required_series_index(arguments, "series_index")
|
||||
ensured = self.ensure_book(title, author_id, series_id, series_index)
|
||||
return [self.book_result(ensured.book, ensured.action)]
|
||||
|
||||
@@ -338,7 +333,7 @@ class CatalogToolRegistry:
|
||||
title: str,
|
||||
author_id: int,
|
||||
series_id: int | None,
|
||||
series_index: int,
|
||||
series_index: float,
|
||||
) -> EnsuredBook:
|
||||
"""Return an existing book row, or create it after validating ownership."""
|
||||
title = normalize_title_slug(title)
|
||||
@@ -398,6 +393,26 @@ class CatalogToolRegistry:
|
||||
raise MetadataResolutionError(msg)
|
||||
return series
|
||||
|
||||
def find_series_by_catalog_slug(self, name: str, author_id: int) -> AudiobookSeries | None:
|
||||
"""Return a series by exact slug or underscore-insensitive slug."""
|
||||
exact = self.session.scalar(
|
||||
select(AudiobookSeries).where(
|
||||
AudiobookSeries.name == name,
|
||||
AudiobookSeries.author_id == author_id,
|
||||
),
|
||||
)
|
||||
if exact is not None:
|
||||
return exact
|
||||
|
||||
compact_name = compact_catalog_slug(name)
|
||||
series_rows = self.session.scalars(
|
||||
select(AudiobookSeries).where(AudiobookSeries.author_id == author_id).order_by(AudiobookSeries.name),
|
||||
).all()
|
||||
for series in series_rows:
|
||||
if compact_catalog_slug(series.name) == compact_name:
|
||||
return series
|
||||
return None
|
||||
|
||||
def series_result(self, series: AudiobookSeries, action: str) -> dict[str, object]:
|
||||
"""Build a normalized series tool result."""
|
||||
return {
|
||||
@@ -513,6 +528,11 @@ def normalize_catalog_slug(value: str) -> str:
|
||||
return re.sub(r"[^a-z0-9]+", "_", value.strip().casefold()).strip("_")
|
||||
|
||||
|
||||
def compact_catalog_slug(value: str) -> str:
|
||||
"""Return a catalog slug comparison key that ignores underscores."""
|
||||
return normalize_catalog_slug(value).replace("_", "")
|
||||
|
||||
|
||||
def normalize_title_slug(value: str) -> str:
|
||||
"""Normalize noisy book titles into lower kebab-case slugs."""
|
||||
return re.sub(r"[^a-z0-9]+", "-", value.strip().casefold()).strip("-")
|
||||
@@ -533,8 +553,9 @@ def query_terms(query: str) -> tuple[str, ...]:
|
||||
"""Return text variants useful for matching noisy audiobook metadata."""
|
||||
normalized = query.strip().casefold()
|
||||
underscore_slug = normalize_catalog_slug(normalized)
|
||||
compact_slug = compact_catalog_slug(normalized)
|
||||
hyphen_slug = normalize_title_slug(normalized)
|
||||
return tuple(dict.fromkeys(term for term in (normalized, underscore_slug, hyphen_slug) if term))
|
||||
return tuple(dict.fromkeys(term for term in (normalized, underscore_slug, compact_slug, hyphen_slug) if term))
|
||||
|
||||
|
||||
def required_string(data: dict[str, object], key: str) -> str:
|
||||
@@ -555,6 +576,19 @@ def required_int(data: dict[str, object], key: str) -> int:
|
||||
return value
|
||||
|
||||
|
||||
def required_series_index(data: dict[str, object], key: str) -> float:
|
||||
"""Read a required whole-number or half-number series index."""
|
||||
value = data.get(key)
|
||||
if isinstance(value, bool) or not isinstance(value, int | float):
|
||||
msg = f"{key} must be a number"
|
||||
raise MetadataResolutionError(msg)
|
||||
series_index = float(value)
|
||||
if not (series_index * 2).is_integer():
|
||||
msg = f"{key} must be a whole number or .5 increment"
|
||||
raise MetadataResolutionError(msg)
|
||||
return series_index
|
||||
|
||||
|
||||
def optional_int(value: object, key: str) -> int | None:
|
||||
"""Read an optional integer field."""
|
||||
if value is None:
|
||||
|
||||
Reference in New Issue
Block a user