From cbeb7469a12c957c16f70ffa1cbf803d01dc7265 Mon Sep 17 00:00:00 2001 From: Trenton H <797416+stumpylog@users.noreply.github.com> Date: Sun, 29 Mar 2026 14:47:06 -0700 Subject: [PATCH] feat(search): natural date keyword rewriting with Whoosh compat shims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement date/timezone boundary math for natural language date queries: - `created` (DateField): local calendar date to UTC midnight boundaries - `added`/`modified` (DateTimeField): local day boundaries with full offset arithmetic - Whoosh compat shims: compact dates (YYYYMMDDHHmmss) → ISO 8601 - Relative ranges: `[now-7d TO now]` → concrete ISO timestamps - Natural keywords: today, yesterday, this_week, last_week, etc. - Timezone-aware: handles UTC offset arithmetic for datetime fields - Passthrough: bare keywords without field prefixes unchanged Co-Authored-By: Claude Sonnet 4.6 --- src/documents/search/_query.py | 207 +++++++++++++++++++++++ src/documents/search/_schema.py | 12 +- src/documents/search/_tokenizer.py | 12 +- src/documents/tests/search/test_query.py | 179 ++++++++++++++++++++ uv.lock | 113 ++++++++++--- 5 files changed, 488 insertions(+), 35 deletions(-) create mode 100644 src/documents/search/_query.py create mode 100644 src/documents/tests/search/test_query.py diff --git a/src/documents/search/_query.py b/src/documents/search/_query.py new file mode 100644 index 000000000..023d27092 --- /dev/null +++ b/src/documents/search/_query.py @@ -0,0 +1,207 @@ +from __future__ import annotations + +import re +from datetime import UTC +from datetime import date +from datetime import datetime +from datetime import timedelta +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from datetime import tzinfo + +_DATE_ONLY_FIELDS = frozenset({"created"}) + +_DATE_KEYWORDS = frozenset( + { + "today", + "yesterday", + "this_week", + "last_week", + "this_month", + "last_month", + "this_year", + "last_year", + }, +) + +_FIELD_DATE_RE = re.compile( + r"(\w+):(" + "|".join(_DATE_KEYWORDS) + r")\b", +) +_COMPACT_DATE_RE = re.compile(r"\b(\d{14})\b") +_RELATIVE_RANGE_RE = re.compile( + r"\[now([+-]\d+[dhm])?\s+TO\s+now([+-]\d+[dhm])?\]", + re.IGNORECASE, +) + + +def _fmt(dt: datetime) -> str: + return dt.astimezone(UTC).strftime("%Y-%m-%dT%H:%M:%SZ") + + +def _iso_range(lo: datetime, hi: datetime) -> str: + return f"[{_fmt(lo)} TO {_fmt(hi)}]" + + +def _date_only_range(keyword: str, tz: tzinfo) -> str: + """ + For `created` (DateField): use the local calendar date, converted to + midnight UTC boundaries. No offset arithmetic — date only. + """ + + today = datetime.now(tz).date() + + if keyword == "today": + lo = datetime(today.year, today.month, today.day, tzinfo=UTC) + return _iso_range(lo, lo + timedelta(days=1)) + if keyword == "yesterday": + y = today - timedelta(days=1) + lo = datetime(y.year, y.month, y.day, tzinfo=UTC) + hi = datetime(today.year, today.month, today.day, tzinfo=UTC) + return _iso_range(lo, hi) + if keyword == "this_week": + mon = today - timedelta(days=today.weekday()) + lo = datetime(mon.year, mon.month, mon.day, tzinfo=UTC) + return _iso_range(lo, lo + timedelta(weeks=1)) + if keyword == "last_week": + this_mon = today - timedelta(days=today.weekday()) + last_mon = this_mon - timedelta(weeks=1) + lo = datetime(last_mon.year, last_mon.month, last_mon.day, tzinfo=UTC) + hi = datetime(this_mon.year, this_mon.month, this_mon.day, tzinfo=UTC) + return _iso_range(lo, hi) + if keyword == "this_month": + lo = datetime(today.year, today.month, 1, tzinfo=UTC) + if today.month == 12: + hi = datetime(today.year + 1, 1, 1, tzinfo=UTC) + else: + hi = datetime(today.year, today.month + 1, 1, tzinfo=UTC) + return _iso_range(lo, hi) + if keyword == "last_month": + if today.month == 1: + lo = datetime(today.year - 1, 12, 1, tzinfo=UTC) + else: + lo = datetime(today.year, today.month - 1, 1, tzinfo=UTC) + hi = datetime(today.year, today.month, 1, tzinfo=UTC) + return _iso_range(lo, hi) + if keyword == "this_year": + lo = datetime(today.year, 1, 1, tzinfo=UTC) + return _iso_range(lo, datetime(today.year + 1, 1, 1, tzinfo=UTC)) + if keyword == "last_year": + lo = datetime(today.year - 1, 1, 1, tzinfo=UTC) + return _iso_range(lo, datetime(today.year, 1, 1, tzinfo=UTC)) + raise ValueError(f"Unknown keyword: {keyword}") + + +def _datetime_range(keyword: str, tz: tzinfo) -> str: + """ + For `added` / `modified` (DateTimeField, stored as UTC): convert local day + boundaries to UTC — full offset arithmetic required. + """ + + now_local = datetime.now(tz) + today = now_local.date() + + def _midnight(d: date) -> datetime: + return datetime(d.year, d.month, d.day, tzinfo=tz).astimezone(UTC) + + if keyword == "today": + return _iso_range(_midnight(today), _midnight(today + timedelta(days=1))) + if keyword == "yesterday": + y = today - timedelta(days=1) + return _iso_range(_midnight(y), _midnight(today)) + if keyword == "this_week": + mon = today - timedelta(days=today.weekday()) + return _iso_range(_midnight(mon), _midnight(mon + timedelta(weeks=1))) + if keyword == "last_week": + this_mon = today - timedelta(days=today.weekday()) + last_mon = this_mon - timedelta(weeks=1) + return _iso_range(_midnight(last_mon), _midnight(this_mon)) + if keyword == "this_month": + first = today.replace(day=1) + if today.month == 12: + next_first = date(today.year + 1, 1, 1) + else: + next_first = date(today.year, today.month + 1, 1) + return _iso_range(_midnight(first), _midnight(next_first)) + if keyword == "last_month": + this_first = today.replace(day=1) + if today.month == 1: + last_first = date(today.year - 1, 12, 1) + else: + last_first = date(today.year, today.month - 1, 1) + return _iso_range(_midnight(last_first), _midnight(this_first)) + if keyword == "this_year": + return _iso_range( + _midnight(date(today.year, 1, 1)), + _midnight(date(today.year + 1, 1, 1)), + ) + if keyword == "last_year": + return _iso_range( + _midnight(date(today.year - 1, 1, 1)), + _midnight(date(today.year, 1, 1)), + ) + raise ValueError(f"Unknown keyword: {keyword}") + + +def _rewrite_compact_date(query: str) -> str: + def _sub(m: re.Match) -> str: + raw = m.group(1) + try: + dt = datetime( + int(raw[0:4]), + int(raw[4:6]), + int(raw[6:8]), + int(raw[8:10]), + int(raw[10:12]), + int(raw[12:14]), + tzinfo=UTC, + ) + return dt.strftime("%Y-%m-%dT%H:%M:%SZ") + except ValueError: + return m.group(0) + + return _COMPACT_DATE_RE.sub(_sub, query) + + +def _rewrite_relative_range(query: str) -> str: + def _sub(m: re.Match) -> str: + now = datetime.now(UTC) + + def _offset(s: str | None) -> timedelta: + if not s: + return timedelta(0) + sign = 1 if s[0] == "+" else -1 + n, unit = int(s[1:-1]), s[-1] + return ( + sign + * { + "d": timedelta(days=n), + "h": timedelta(hours=n), + "m": timedelta(minutes=n), + }[unit] + ) + + lo, hi = now + _offset(m.group(1)), now + _offset(m.group(2)) + if lo > hi: + lo, hi = hi, lo + return f"[{_fmt(lo)} TO {_fmt(hi)}]" + + return _RELATIVE_RANGE_RE.sub(_sub, query) + + +def rewrite_natural_date_keywords(query: str, tz: tzinfo) -> str: + """ + Preprocessing stage 1: rewrite Whoosh compact dates, relative ranges, + and natural date keywords (field:today etc.) to ISO 8601. + Bare keywords without a field: prefix pass through unchanged. + """ + query = _rewrite_compact_date(query) + query = _rewrite_relative_range(query) + + def _replace(m: re.Match) -> str: + field, keyword = m.group(1), m.group(2) + if field in _DATE_ONLY_FIELDS: + return f"{field}:{_date_only_range(keyword, tz)}" + return f"{field}:{_datetime_range(keyword, tz)}" + + return _FIELD_DATE_RE.sub(_replace, query) diff --git a/src/documents/search/_schema.py b/src/documents/search/_schema.py index dd5863eea..3a68baa7b 100644 --- a/src/documents/search/_schema.py +++ b/src/documents/search/_schema.py @@ -31,7 +31,7 @@ def build_schema() -> tantivy.Schema: ): sb.add_text_field(field, stored=True, tokenizer_name="paperless_text") - # Shadow sort fields — fast, not stored/indexed + # Shadow sort fields - fast, not stored/indexed for field in ("title_sort", "correspondent_sort", "type_sort"): sb.add_text_field( field, @@ -40,10 +40,10 @@ def build_schema() -> tantivy.Schema: fast=True, ) - # CJK support — not stored, indexed only + # CJK support - not stored, indexed only sb.add_text_field("bigram_content", stored=False, tokenizer_name="bigram_analyzer") - # Autocomplete prefix scan — stored, not indexed + # Autocomplete prefix scan - stored, not indexed sb.add_text_field("autocomplete_word", stored=True, tokenizer_name="raw") sb.add_text_field("tag", stored=True, tokenizer_name="paperless_text") @@ -75,17 +75,17 @@ def _needs_rebuild(index_dir: Path) -> bool: return True try: if int(version_file.read_text().strip()) != SCHEMA_VERSION: - logger.info("Search index schema version mismatch — rebuilding.") + logger.info("Search index schema version mismatch - rebuilding.") return True except ValueError: return True language_file = index_dir / ".schema_language" if not language_file.exists(): - logger.info("Search index language sentinel missing — rebuilding.") + logger.info("Search index language sentinel missing - rebuilding.") return True if language_file.read_text().strip() != settings.SEARCH_LANGUAGE: - logger.info("Search index language changed — rebuilding.") + logger.info("Search index language changed - rebuilding.") return True return False diff --git a/src/documents/search/_tokenizer.py b/src/documents/search/_tokenizer.py index f15615d99..1bcd91113 100644 --- a/src/documents/search/_tokenizer.py +++ b/src/documents/search/_tokenizer.py @@ -6,7 +6,7 @@ import tantivy logger = logging.getLogger("paperless.search") -# Mapping of ISO 639-1 codes (and common aliases) → Tantivy Snowball name +# Mapping of ISO 639-1 codes (and common aliases) -> Tantivy Snowball name _LANGUAGE_MAP: dict[str, str] = { "ar": "Arabic", "arabic": "Arabic", @@ -52,7 +52,7 @@ SUPPORTED_LANGUAGES: frozenset[str] = frozenset(_LANGUAGE_MAP) def register_tokenizers(index: tantivy.Index, language: str) -> None: """ Register all custom tokenizers on *index*. Must be called on every Index - instance — tantivy requires re-registration at each open. + instance - tantivy requires re-registration at each open. """ index.register_tokenizer("paperless_text", _paperless_text(language)) index.register_tokenizer("simple_analyzer", _simple_analyzer()) @@ -60,7 +60,7 @@ def register_tokenizers(index: tantivy.Index, language: str) -> None: def _paperless_text(language: str) -> tantivy.TextAnalyzer: - """simple → remove_long(65) → lowercase → ascii_fold [→ stemmer]""" + """simple -> remove_long(65) -> lowercase -> ascii_fold [-> stemmer]""" builder = ( tantivy.TextAnalyzerBuilder(tantivy.Tokenizer.simple()) .filter(tantivy.Filter.remove_long(65)) @@ -73,7 +73,7 @@ def _paperless_text(language: str) -> tantivy.TextAnalyzer: builder = builder.filter(tantivy.Filter.stemmer(tantivy_lang)) else: logger.warning( - "Unsupported search language '%s' — stemming disabled. Supported: %s", + "Unsupported search language '%s' - stemming disabled. Supported: %s", language, ", ".join(sorted(SUPPORTED_LANGUAGES)), ) @@ -81,7 +81,7 @@ def _paperless_text(language: str) -> tantivy.TextAnalyzer: def _simple_analyzer() -> tantivy.TextAnalyzer: - """simple → lowercase → ascii_fold. Used for shadow sort fields.""" + """simple -> lowercase -> ascii_fold. Used for shadow sort fields.""" return ( tantivy.TextAnalyzerBuilder(tantivy.Tokenizer.simple()) .filter(tantivy.Filter.lowercase()) @@ -91,7 +91,7 @@ def _simple_analyzer() -> tantivy.TextAnalyzer: def _bigram_analyzer() -> tantivy.TextAnalyzer: - """ngram(2,2) → lowercase. CJK / no-whitespace language support.""" + """ngram(2,2) -> lowercase. CJK / no-whitespace language support.""" return ( tantivy.TextAnalyzerBuilder( tantivy.Tokenizer.ngram(min_gram=2, max_gram=2, prefix_only=False), diff --git a/src/documents/tests/search/test_query.py b/src/documents/tests/search/test_query.py new file mode 100644 index 000000000..d3735e9e7 --- /dev/null +++ b/src/documents/tests/search/test_query.py @@ -0,0 +1,179 @@ +from __future__ import annotations + +import re +from datetime import UTC +from datetime import datetime +from zoneinfo import ZoneInfo + +import pytest +import time_machine + +from documents.search._query import rewrite_natural_date_keywords + +pytestmark = pytest.mark.search + +UTC = UTC +EASTERN = ZoneInfo("America/New_York") # UTC-5 / UTC-4 (DST) +AUCKLAND = ZoneInfo("Pacific/Auckland") # UTC+13 in southern-hemisphere summer + + +def _range(result: str, field: str) -> tuple[str, str]: + m = re.search(rf"{field}:\[(.+?) TO (.+?)\]", result) + assert m, f"No range for {field!r} in: {result!r}" + return m.group(1), m.group(2) + + +class TestCreatedDateField: + """ + created is a Django DateField: indexed as midnight UTC of the local calendar + date. No offset arithmetic needed - the local calendar date is what matters. + """ + + @pytest.mark.parametrize( + ("tz", "expected_lo", "expected_hi"), + [ + pytest.param(UTC, "2026-03-28T00:00:00Z", "2026-03-29T00:00:00Z", id="utc"), + pytest.param( + EASTERN, + "2026-03-28T00:00:00Z", + "2026-03-29T00:00:00Z", + id="eastern_same_calendar_date", + ), + ], + ) + @time_machine.travel(datetime(2026, 3, 28, 15, 30, tzinfo=UTC), tick=False) + def test_today(self, tz, expected_lo, expected_hi): + lo, hi = _range(rewrite_natural_date_keywords("created:today", tz), "created") + assert lo == expected_lo + assert hi == expected_hi + + @time_machine.travel(datetime(2026, 3, 28, 3, 0, tzinfo=UTC), tick=False) + def test_today_auckland_ahead_of_utc(self): + # UTC 03:00 -> Auckland (UTC+13) = 16:00 same date; local date = 2026-03-28 + lo, _ = _range( + rewrite_natural_date_keywords("created:today", AUCKLAND), + "created", + ) + assert lo == "2026-03-28T00:00:00Z" + + @pytest.mark.parametrize( + ("field", "keyword", "expected_lo", "expected_hi"), + [ + pytest.param( + "created", + "yesterday", + "2026-03-27T00:00:00Z", + "2026-03-28T00:00:00Z", + id="yesterday", + ), + pytest.param( + "created", + "this_week", + "2026-03-23T00:00:00Z", + "2026-03-30T00:00:00Z", + id="this_week_mon_sun", + ), + pytest.param( + "created", + "last_week", + "2026-03-16T00:00:00Z", + "2026-03-23T00:00:00Z", + id="last_week", + ), + pytest.param( + "created", + "this_month", + "2026-03-01T00:00:00Z", + "2026-04-01T00:00:00Z", + id="this_month", + ), + pytest.param( + "created", + "last_month", + "2026-02-01T00:00:00Z", + "2026-03-01T00:00:00Z", + id="last_month", + ), + pytest.param( + "created", + "this_year", + "2026-01-01T00:00:00Z", + "2027-01-01T00:00:00Z", + id="this_year", + ), + pytest.param( + "created", + "last_year", + "2025-01-01T00:00:00Z", + "2026-01-01T00:00:00Z", + id="last_year", + ), + ], + ) + @time_machine.travel(datetime(2026, 3, 28, 15, 0, tzinfo=UTC), tick=False) + def test_date_keywords(self, field, keyword, expected_lo, expected_hi): + # 2026-03-28 is Saturday; Mon-Sun week calculation built into expectations + query = f"{field}:{keyword}" + lo, hi = _range(rewrite_natural_date_keywords(query, UTC), field) + assert lo == expected_lo + assert hi == expected_hi + + +class TestDateTimeFields: + """ + added/modified store full UTC datetimes. Natural keywords must convert + the local day boundaries to UTC - timezone offset arithmetic IS required. + """ + + @time_machine.travel(datetime(2026, 3, 28, 15, 30, tzinfo=UTC), tick=False) + def test_added_today_eastern(self): + # EDT = UTC-4; local midnight 2026-03-28 00:00 EDT = 2026-03-28 04:00 UTC + lo, hi = _range(rewrite_natural_date_keywords("added:today", EASTERN), "added") + assert lo == "2026-03-28T04:00:00Z" + assert hi == "2026-03-29T04:00:00Z" + + @time_machine.travel(datetime(2026, 3, 29, 2, 0, tzinfo=UTC), tick=False) + def test_added_today_auckland_midnight_crossing(self): + # UTC 02:00 on 2026-03-29 -> Auckland (UTC+13) = 2026-03-29 15:00 local + # Auckland midnight = UTC 2026-03-28 11:00 + lo, hi = _range(rewrite_natural_date_keywords("added:today", AUCKLAND), "added") + assert lo == "2026-03-28T11:00:00Z" + assert hi == "2026-03-29T11:00:00Z" + + @time_machine.travel(datetime(2026, 3, 28, 15, 0, tzinfo=UTC), tick=False) + def test_modified_today_utc(self): + lo, hi = _range( + rewrite_natural_date_keywords("modified:today", UTC), + "modified", + ) + assert lo == "2026-03-28T00:00:00Z" + assert hi == "2026-03-29T00:00:00Z" + + +class TestWhooshCompatShims: + """Whoosh compact dates and relative ranges must be converted to ISO format.""" + + @time_machine.travel(datetime(2026, 3, 28, 15, 0, tzinfo=UTC), tick=False) + def test_compact_date_shim_rewrites_to_iso(self): + # Whoosh compact: YYYYMMDDHHmmss + result = rewrite_natural_date_keywords("created:20240115120000", UTC) + assert "2024-01-15" in result + assert "20240115120000" not in result + + @time_machine.travel(datetime(2026, 3, 28, 15, 0, tzinfo=UTC), tick=False) + def test_relative_range_shim_removes_now(self): + result = rewrite_natural_date_keywords("added:[now-7d TO now]", UTC) + assert "now" not in result + assert "2026-03-" in result + + +class TestPassthrough: + """Queries without field prefixes or unrelated content pass through unchanged.""" + + def test_bare_keyword_no_field_prefix_unchanged(self): + # Bare 'today' with no field: prefix passes through unchanged + result = rewrite_natural_date_keywords("bank statement today", UTC) + assert "today" in result + + def test_unrelated_query_unchanged(self): + assert rewrite_natural_date_keywords("title:invoice", UTC) == "title:invoice" diff --git a/uv.lock b/uv.lock index 53cbeb689..a69875931 100644 --- a/uv.lock +++ b/uv.lock @@ -350,15 +350,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/73/3183c9e41ca755713bdf2cc1d0810df742c09484e2e1ddd693bee53877c1/brotli-1.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2d085ded05278d1c7f65560aae97b3160aeb2ea2c0b3e26204856beccb60888", size = 1488164, upload-time = "2025-11-05T18:38:53.079Z" }, ] -[[package]] -name = "cached-property" -version = "2.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/4b/3d870836119dbe9a5e3c9a61af8cc1a8b69d75aea564572e385882d5aefb/cached_property-2.0.1.tar.gz", hash = "sha256:484d617105e3ee0e4f1f58725e72a8ef9e93deee462222dbd51cd91230897641", size = 10574, upload-time = "2024-10-25T15:43:55.667Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/0e/7d8225aab3bc1a0f5811f8e1b557aa034ac04bdf641925b30d3caf586b28/cached_property-2.0.1-py3-none-any.whl", hash = "sha256:f617d70ab1100b7bcf6e42228f9ddcb78c676ffa167278d9f730d1c2fba69ccb", size = 7428, upload-time = "2024-10-25T15:43:54.711Z" }, -] - [[package]] name = "cbor2" version = "5.9.0" @@ -2910,12 +2901,12 @@ dependencies = [ { name = "scikit-learn", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "sentence-transformers", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "setproctitle", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "tantivy", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "tika-client", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, { name = "torch", version = "2.10.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'linux'" }, { name = "watchfiles", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "whitenoise", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, - { name = "whoosh-reloaded", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "zxing-cpp", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] @@ -2951,6 +2942,7 @@ dev = [ { name = "pytest-sugar", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pytest-xdist", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "ruff", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "time-machine", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "zensical", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] docs = [ @@ -2974,6 +2966,7 @@ testing = [ { name = "pytest-rerunfailures", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pytest-sugar", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, { name = "pytest-xdist", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, + { name = "time-machine", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, ] typing = [ { name = "celery-types", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, @@ -3064,11 +3057,11 @@ requires-dist = [ { name = "scikit-learn", specifier = "~=1.8.0" }, { name = "sentence-transformers", specifier = ">=4.1" }, { name = "setproctitle", specifier = "~=1.3.4" }, + { name = "tantivy", specifier = ">=0.25.1" }, { name = "tika-client", specifier = "~=0.10.0" }, { name = "torch", specifier = "~=2.10.0", index = "https://download.pytorch.org/whl/cpu" }, { name = "watchfiles", specifier = ">=1.1.1" }, { name = "whitenoise", specifier = "~=6.11" }, - { name = "whoosh-reloaded", specifier = ">=2.7.5" }, { name = "zxing-cpp", specifier = "~=3.0.0" }, ] provides-extras = ["mariadb", "postgres", "webserver"] @@ -3090,6 +3083,7 @@ dev = [ { name = "pytest-sugar" }, { name = "pytest-xdist", specifier = "~=3.8.0" }, { name = "ruff", specifier = "~=0.15.0" }, + { name = "time-machine", specifier = ">=2.13" }, { name = "zensical", specifier = ">=0.0.21" }, ] docs = [{ name = "zensical", specifier = ">=0.0.21" }] @@ -3111,6 +3105,7 @@ testing = [ { name = "pytest-rerunfailures", specifier = "~=16.1" }, { name = "pytest-sugar" }, { name = "pytest-xdist", specifier = "~=3.8.0" }, + { name = "time-machine", specifier = ">=2.13" }, ] typing = [ { name = "celery-types" }, @@ -4664,6 +4659,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] +[[package]] +name = "tantivy" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/f9/0cd3955d155d3e3ef74b864769514dd191e5dacba9f0beb7af2d914942ce/tantivy-0.25.1.tar.gz", hash = "sha256:68a3314699a7d18fcf338b52bae8ce46a97dde1128a3e47e33fa4db7f71f265e", size = 75120, upload-time = "2025-12-02T11:57:12.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/7a/8a277f377e8a151fc0e71d4ffc1114aefb6e5e1c7dd609fed0955cf34ed8/tantivy-0.25.1-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d363d7b4207d3a5aa7f0d212420df35bed18bdb6bae26a2a8bd57428388b7c29", size = 7637033, upload-time = "2025-12-02T11:56:18.104Z" }, + { url = "https://files.pythonhosted.org/packages/71/31/8b4acdedfc9f9a2d04b1340d07eef5213d6f151d1e18da0cb423e5f090d2/tantivy-0.25.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8f4389cf1d889a1df7c5a3195806b4b56c37cee10d8a26faaa0dea35a867b5ff", size = 3932180, upload-time = "2025-12-02T11:56:19.833Z" }, + { url = "https://files.pythonhosted.org/packages/2f/dc/3e8499c21b4b9795e8f2fc54c68ce5b92905aaeadadaa56ecfa9180b11b1/tantivy-0.25.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99864c09fc54652c3c2486cdf13f86cdc8200f4b481569cb291e095ca5d496e5", size = 4197620, upload-time = "2025-12-02T11:56:21.496Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8e/f2ce62fffc811eb62bead92c7b23c2e218f817cbd54c4f3b802e03ba1438/tantivy-0.25.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05abf37ddbc5063c575548be0d62931629c086bff7a5a1b67cf5a8f5ebf4cd8c", size = 4183794, upload-time = "2025-12-02T11:56:23.215Z" }, + { url = "https://files.pythonhosted.org/packages/41/e7/6849c713ed0996c7628324c60512c4882006f0a62145e56c624a93407f90/tantivy-0.25.1-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:90fd919e5f611809f746560ecf36eb9be824dec62e21ae17a27243759edb9aa1", size = 7621494, upload-time = "2025-12-02T11:56:27.069Z" }, + { url = "https://files.pythonhosted.org/packages/c5/22/c3d8294600dc6e7fa350daef9ff337d3c06e132b81df727de9f7a50c692a/tantivy-0.25.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:4613c7cf6c23f3a97989819690a0f956d799354957de7a204abcc60083cebe02", size = 3925219, upload-time = "2025-12-02T11:56:29.403Z" }, + { url = "https://files.pythonhosted.org/packages/41/fc/cbb1df71dd44c9110eff4eaaeda9d44f2d06182fe0452193be20ddfba93f/tantivy-0.25.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c477bd20b4df804d57dfc5033431bef27cde605695ae141b03abbf6ebc069129", size = 4198699, upload-time = "2025-12-02T11:56:31.359Z" }, + { url = "https://files.pythonhosted.org/packages/47/4d/71abb78b774073c3ce12a4faa4351a9d910a71ffa3659526affba163873d/tantivy-0.25.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9b1a1ba1113c523c7ff7b10f282d6c4074006f7ef8d71e1d973d51bf7291ddb", size = 4183585, upload-time = "2025-12-02T11:56:33.317Z" }, + { url = "https://files.pythonhosted.org/packages/3d/25/73cfbcf1a8ea49be6c42817431cac46b70a119fe64da903fcc2d92b5b511/tantivy-0.25.1-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f51ff7196c6f31719202080ed8372d5e3d51e92c749c032fb8234f012e99744c", size = 7622530, upload-time = "2025-12-02T11:56:36.839Z" }, + { url = "https://files.pythonhosted.org/packages/12/c8/c0d7591cdf4f7e7a9fc4da786d1ca8cd1aacffaa2be16ea6d401a8e4a566/tantivy-0.25.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:550e63321bfcacc003859f2fa29c1e8e56450807b3c9a501c1add27cfb9236d9", size = 3925637, upload-time = "2025-12-02T11:56:38.425Z" }, + { url = "https://files.pythonhosted.org/packages/3a/09/bedfc223bffec7641b417dd7ab071134b2ef8f8550e9b1fb6014657ef52e/tantivy-0.25.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fde31cc8d6e122faf7902aeea32bc008a429a6e8904e34d3468126a3ec01b016", size = 4197322, upload-time = "2025-12-02T11:56:40.411Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f1/1fa5183500c8042200c9f2b840d34f5bbcfb434a1ee750e7132262d2a5c9/tantivy-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b11bd5a518b0be645320b47af8493f6a40c4f3234313e37adcf4534a564d27dd", size = 4183143, upload-time = "2025-12-02T11:56:42.048Z" }, + { url = "https://files.pythonhosted.org/packages/8b/2f/581519492226f97d23bd0adc95dad991ebeaa73ea6abc8bff389a3096d9a/tantivy-0.25.1-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:dae99e75b7eaa9bf5bd16ab106b416370f08c135aed0e117d62a3201cd1ffe36", size = 7610316, upload-time = "2025-12-02T11:56:45.927Z" }, + { url = "https://files.pythonhosted.org/packages/91/40/5d7bc315ab9e6a22c5572656e8ada1c836cfa96dccf533377504fbc3c9d9/tantivy-0.25.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:506e9533c5ef4d3df43bad64ffecc0aa97c76e361ea610815dc3a20a9d6b30b3", size = 3919882, upload-time = "2025-12-02T11:56:48.469Z" }, + { url = "https://files.pythonhosted.org/packages/02/b9/e0ef2f57a6a72444cb66c2ffbc310ab33ffaace275f1c4b0319d84ea3f18/tantivy-0.25.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dbd4f8f264dacbcc9dee542832da2173fd53deaaea03f082d95214f8b5ed6bc", size = 4196031, upload-time = "2025-12-02T11:56:50.151Z" }, + { url = "https://files.pythonhosted.org/packages/1e/02/bf3f8cacfd08642e14a73f7956a3fb95d58119132c98c121b9065a1f8615/tantivy-0.25.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:824c643ccb640dd9e35e00c5d5054ddf3323f56fe4219d57d428a9eeea13d22c", size = 4183437, upload-time = "2025-12-02T11:56:51.818Z" }, + { url = "https://files.pythonhosted.org/packages/ff/44/9f1d67aa5030f7eebc966c863d1316a510a971dd8bb45651df4acdfae9ed/tantivy-0.25.1-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7f5d29ae85dd0f23df8d15b3e7b341d4f9eb5a446bbb9640df48ac1f6d9e0c6c", size = 7623723, upload-time = "2025-12-02T11:56:55.066Z" }, + { url = "https://files.pythonhosted.org/packages/db/30/6e085bd3ed9d12da3c91c185854abd70f9dfd35fb36a75ea98428d42c30b/tantivy-0.25.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:f2d2938fb69a74fc1bb36edfaf7f0d1596fa1264db0f377bda2195c58bcb6245", size = 3926243, upload-time = "2025-12-02T11:56:57.058Z" }, + { url = "https://files.pythonhosted.org/packages/32/f5/a00d65433430f51718e5cc6938df571765d7c4e03aedec5aef4ab567aa9b/tantivy-0.25.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f5ff124c4802558e627091e780b362ca944169736caba5a372eef39a79d0ae0", size = 4207186, upload-time = "2025-12-02T11:56:58.803Z" }, + { url = "https://files.pythonhosted.org/packages/19/63/61bdb12fc95f2a7f77bd419a5149bfa9f28caa76cb569bf2b6b06e1d033e/tantivy-0.25.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43b80ef62a340416139c93d19264e5f808da48e04f9305f1092b8ed22be0a5be", size = 4187312, upload-time = "2025-12-02T11:57:00.595Z" }, +] + [[package]] name = "tenacity" version = "9.1.2" @@ -4752,6 +4775,62 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, ] +[[package]] +name = "time-machine" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/fc/37b02f6094dbb1f851145330460532176ed2f1dc70511a35828166c41e52/time_machine-3.2.0.tar.gz", hash = "sha256:a4ddd1cea17b8950e462d1805a42b20c81eb9aafc8f66b392dd5ce997e037d79", size = 14804, upload-time = "2025-12-17T23:33:02.599Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f5/e1/03aae5fbaa53859f665094af696338fc7cae733d926a024af69982712350/time_machine-3.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c188a9dda9fcf975022f1b325b466651b96a4dfc223c523ed7ed8d979f9bf3e8", size = 19143, upload-time = "2025-12-17T23:31:44.258Z" }, + { url = "https://files.pythonhosted.org/packages/75/8f/98cb17bebb52b22ff4ec26984dd44280f9c71353c3bae0640a470e6683e5/time_machine-3.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:17245f1cc2dd13f9d63a174be59bb2684a9e5e0a112ab707e37be92068cd655f", size = 15273, upload-time = "2025-12-17T23:31:45.246Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2f/ca11e4a7897234bb9331fcc5f4ed4714481ba4012370cc79a0ae8c42ea0a/time_machine-3.2.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d9bd1de1996e76efd36ae15970206c5089fb3728356794455bd5cd8d392b5537", size = 31049, upload-time = "2025-12-17T23:31:46.613Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ad/d17d83a59943094e6b6c6a3743caaf6811b12203c3e07a30cc7bcc2ab7ee/time_machine-3.2.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98493cd50e8b7f941eab69b9e18e697ad69db1a0ec1959f78f3d7b0387107e5c", size = 32632, upload-time = "2025-12-17T23:31:47.72Z" }, + { url = "https://files.pythonhosted.org/packages/71/50/d60576d047a0dfb5638cdfb335e9c3deb6e8528544fa0b3966a8480f72b7/time_machine-3.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31f2a33d595d9f91eb9bc7f157f0dc5721f5789f4c4a9e8b852cdedb2a7d9b16", size = 34289, upload-time = "2025-12-17T23:31:48.913Z" }, + { url = "https://files.pythonhosted.org/packages/fa/fe/4afa602dbdebddde6d0ea4a7fe849e49b9bb85dc3fb415725a87ccb4b471/time_machine-3.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9f78ac4213c10fbc44283edd1a29cfb7d3382484f4361783ddc057292aaa1889", size = 33175, upload-time = "2025-12-17T23:31:50.611Z" }, + { url = "https://files.pythonhosted.org/packages/0d/87/c152e23977c1d7d7c94eb3ed3ea45cc55971796205125c6fdff40db2c60f/time_machine-3.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c1326b09e947b360926d529a96d1d9e126ce120359b63b506ecdc6ee20755c23", size = 31170, upload-time = "2025-12-17T23:31:51.645Z" }, + { url = "https://files.pythonhosted.org/packages/80/af/54acf51d0f3ade3b51eab73df6192937c9a938753ef5456dff65eb8630be/time_machine-3.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9f2949f03d15264cc15c38918a2cda8966001f0f4ebe190cbfd9c56d91aed8ac", size = 32292, upload-time = "2025-12-17T23:31:52.803Z" }, + { url = "https://files.pythonhosted.org/packages/71/8b/080c8eedcd67921a52ba5bd0e075362062509ab63c86fc1a0442fad241a6/time_machine-3.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc4bee5b0214d7dc4ebc91f4a4c600f1a598e9b5606ac751f42cb6f6740b1dbb", size = 19255, upload-time = "2025-12-17T23:31:58.057Z" }, + { url = "https://files.pythonhosted.org/packages/66/17/0e5291e9eb705bf8a5a1305f826e979af307bbeb79def4ddbf4b3f9a81e0/time_machine-3.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ca036304b4460ae2fdc1b52dd8b1fa7cf1464daa427fc49567413c09aa839c1", size = 15360, upload-time = "2025-12-17T23:31:59.048Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/9ab87b71d2e2b62463b9b058b7ae7ac09fb57f8fcd88729dec169d304340/time_machine-3.2.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5442735b41d7a2abc2f04579b4ca6047ed4698a8338a4fec92c7c9423e7938cb", size = 33029, upload-time = "2025-12-17T23:32:00.413Z" }, + { url = "https://files.pythonhosted.org/packages/4b/26/b5ca19da6f25ea905b3e10a0ea95d697c1aeba0404803a43c68f1af253e6/time_machine-3.2.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:97da3e971e505cb637079fb07ab0bcd36e33279f8ecac888ff131f45ef1e4d8d", size = 34579, upload-time = "2025-12-17T23:32:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/79/ca/6ac7ad5f10ea18cc1d9de49716ba38c32132c7b64532430d92ef240c116b/time_machine-3.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3cdda6dee4966e38aeb487309bb414c6cb23a81fc500291c77a8fcd3098832e7", size = 35961, upload-time = "2025-12-17T23:32:02.521Z" }, + { url = "https://files.pythonhosted.org/packages/33/67/390dd958bed395ab32d79a9fe61fe111825c0dd4ded54dbba7e867f171e6/time_machine-3.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:33d9efd302a6998bcc8baa4d84f259f8a4081105bd3d7f7af7f1d0abd3b1c8aa", size = 34668, upload-time = "2025-12-17T23:32:03.585Z" }, + { url = "https://files.pythonhosted.org/packages/da/57/c88fff034a4e9538b3ae7c68c9cfb283670b14d17522c5a8bc17d29f9a4b/time_machine-3.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3a0b0a33971f14145853c9bd95a6ab0353cf7e0019fa2a7aa1ae9fddfe8eab50", size = 32891, upload-time = "2025-12-17T23:32:04.656Z" }, + { url = "https://files.pythonhosted.org/packages/2d/70/ebbb76022dba0fec8f9156540fc647e4beae1680c787c01b1b6200e56d70/time_machine-3.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2d0be9e5f22c38082d247a2cdcd8a936504e9db60b7b3606855fb39f299e9548", size = 34080, upload-time = "2025-12-17T23:32:06.146Z" }, + { url = "https://files.pythonhosted.org/packages/ee/cd/43ad5efc88298af3c59b66769cea7f055567a85071579ed40536188530c1/time_machine-3.2.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c421a8eb85a4418a7675a41bf8660224318c46cc62e4751c8f1ceca752059090", size = 19318, upload-time = "2025-12-17T23:32:10.518Z" }, + { url = "https://files.pythonhosted.org/packages/b0/f6/084010ef7f4a3f38b5a4900923d7c85b29e797655c4f6ee4ce54d903cca8/time_machine-3.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f4e758f7727d0058c4950c66b58200c187072122d6f7a98b610530a4233ea7b", size = 15390, upload-time = "2025-12-17T23:32:11.625Z" }, + { url = "https://files.pythonhosted.org/packages/25/aa/1cabb74134f492270dc6860cb7865859bf40ecf828be65972827646e91ad/time_machine-3.2.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:154bd3f75c81f70218b2585cc12b60762fb2665c507eec5ec5037d8756d9b4e0", size = 33115, upload-time = "2025-12-17T23:32:13.219Z" }, + { url = "https://files.pythonhosted.org/packages/5e/03/78c5d7dfa366924eb4dbfcc3fc917c39a4280ca234b12819cc1f16c03d88/time_machine-3.2.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d50cfe5ebea422c896ad8d278af9648412b7533b8ea6adeeee698a3fd9b1d3b7", size = 34705, upload-time = "2025-12-17T23:32:14.29Z" }, + { url = "https://files.pythonhosted.org/packages/86/93/d5e877c24541f674c6869ff6e9c56833369796010190252e92c9d7ae5f0f/time_machine-3.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:636576501724bd6a9124e69d86e5aef263479e89ef739c5db361469f0463a0a1", size = 36104, upload-time = "2025-12-17T23:32:15.354Z" }, + { url = "https://files.pythonhosted.org/packages/22/1c/d4bae72f388f67efc9609f89b012e434bb19d9549c7a7b47d6c7d9e5c55d/time_machine-3.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:40e6f40c57197fcf7ec32d2c563f4df0a82c42cdcc3cab27f688e98f6060df10", size = 34765, upload-time = "2025-12-17T23:32:16.434Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c3/ac378cf301d527d8dfad2f0db6bad0dfb1ab73212eaa56d6b96ee5d9d20b/time_machine-3.2.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a1bcf0b846bbfc19a79bc19e3fa04d8c7b1e8101c1b70340ffdb689cd801ea53", size = 33010, upload-time = "2025-12-17T23:32:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/06/35/7ce897319accda7a6970b288a9a8c52d25227342a7508505a2b3d235b649/time_machine-3.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ae55a56c179f4fe7a62575ad5148b6ed82f6c7e5cf2f9a9ec65f2f5b067db5f5", size = 34185, upload-time = "2025-12-17T23:32:18.566Z" }, + { url = "https://files.pythonhosted.org/packages/67/e7/487f0ba5fe6c58186a5e1af2a118dfa2c160fedb37ef53a7e972d410408e/time_machine-3.2.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:59d71545e62525a4b85b6de9ab5c02ee3c61110fd7f636139914a2335dcbfc9c", size = 20000, upload-time = "2025-12-17T23:32:23.058Z" }, + { url = "https://files.pythonhosted.org/packages/e1/17/eb2c0054c8d44dd42df84ccd434539249a9c7d0b8eb53f799be2102500ab/time_machine-3.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:999672c621c35362bc28e03ca0c7df21500195540773c25993421fd8d6cc5003", size = 15657, upload-time = "2025-12-17T23:32:24.125Z" }, + { url = "https://files.pythonhosted.org/packages/43/21/93443b5d1dd850f8bb9442e90d817a9033dcce6bfbdd3aabbb9786251c80/time_machine-3.2.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5faf7397f0580c7b9d67288522c8d7863e85f0cffadc0f1fccdb2c3dfce5783e", size = 39216, upload-time = "2025-12-17T23:32:25.542Z" }, + { url = "https://files.pythonhosted.org/packages/9f/9e/18544cf8acc72bb1dc03762231c82ecc259733f4bb6770a7bbe5cd138603/time_machine-3.2.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3dd886ec49f1fa5a00e844f5947e5c0f98ce574750c24b7424c6f77fc1c3e87", size = 40764, upload-time = "2025-12-17T23:32:26.643Z" }, + { url = "https://files.pythonhosted.org/packages/27/f7/9fe9ce2795636a3a7467307af6bdf38bb613ddb701a8a5cd50ec713beb5e/time_machine-3.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da0ecd96bc7bbe450acaaabe569d84e81688f1be8ad58d1470e42371d145fb53", size = 43526, upload-time = "2025-12-17T23:32:27.693Z" }, + { url = "https://files.pythonhosted.org/packages/03/c1/a93e975ba9dec22e87ec92d18c28e67d36bd536f9119ffa439b2892b0c9c/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:158220e946c1c4fb8265773a0282c88c35a7e3bb5d78e3561214e3b3231166f3", size = 41727, upload-time = "2025-12-17T23:32:28.985Z" }, + { url = "https://files.pythonhosted.org/packages/5f/fb/e3633e5a6bbed1c76bb2e9810dabc2f8467532ffcd29b9aed404b473061a/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8c1aee29bc54356f248d5d7dfdd131e12ca825e850a08c0ebdb022266d073013", size = 38952, upload-time = "2025-12-17T23:32:30.031Z" }, + { url = "https://files.pythonhosted.org/packages/82/3d/02e9fb2526b3d6b1b45bc8e4d912d95d1cd699d1a3f6df985817d37a0600/time_machine-3.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c8ed2224f09d25b1c2fc98683613aca12f90f682a427eabb68fc824d27014e4a", size = 39829, upload-time = "2025-12-17T23:32:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/61/70/b4b980d126ed155c78d1879c50d60c8dcbd47bd11cb14ee7be50e0dfc07f/time_machine-3.2.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:1398980c017fe5744d66f419e0115ee48a53b00b146d738e1416c225eb610b82", size = 19303, upload-time = "2025-12-17T23:32:35.796Z" }, + { url = "https://files.pythonhosted.org/packages/73/73/eaa33603c69a68fe2b6f54f9dd75481693d62f1d29676531002be06e2d1c/time_machine-3.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4f8f4e35f4191ef70c2ab8ff490761ee9051b891afce2bf86dde3918eb7b537b", size = 15431, upload-time = "2025-12-17T23:32:37.244Z" }, + { url = "https://files.pythonhosted.org/packages/76/10/b81e138e86cc7bab40cdb59d294b341e172201f4a6c84bb0ec080407977a/time_machine-3.2.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6db498686ecf6163c5aa8cf0bcd57bbe0f4081184f247edf3ee49a2612b584f9", size = 33206, upload-time = "2025-12-17T23:32:38.713Z" }, + { url = "https://files.pythonhosted.org/packages/d3/72/4deab446b579e8bd5dca91de98595c5d6bd6a17ce162abf5c5f2ce40d3d8/time_machine-3.2.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:027c1807efb74d0cd58ad16524dec94212fbe900115d70b0123399883657ac0f", size = 34792, upload-time = "2025-12-17T23:32:40.223Z" }, + { url = "https://files.pythonhosted.org/packages/2c/39/439c6b587ddee76d533fe972289d0646e0a5520e14dc83d0a30aeb5565f7/time_machine-3.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92432610c05676edd5e6946a073c6f0c926923123ce7caee1018dc10782c713d", size = 36187, upload-time = "2025-12-17T23:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/4b/db/2da4368db15180989bab83746a857bde05ad16e78f326801c142bb747a06/time_machine-3.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c25586b62480eb77ef3d953fba273209478e1ef49654592cd6a52a68dfe56a67", size = 34855, upload-time = "2025-12-17T23:32:42.817Z" }, + { url = "https://files.pythonhosted.org/packages/88/84/120a431fee50bc4c241425bee4d3a4910df4923b7ab5f7dff1bf0c772f08/time_machine-3.2.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6bf3a2fa738d15e0b95d14469a0b8ea42635467408d8b490e263d5d45c9a177f", size = 33222, upload-time = "2025-12-17T23:32:43.94Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ea/89cfda82bb8c57ff91bb9a26751aa234d6d90e9b4d5ab0ad9dce0f9f0329/time_machine-3.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ce76b82276d7ad2a66cdc85dad4df19d1422b69183170a34e8fbc4c3f35502f7", size = 34270, upload-time = "2025-12-17T23:32:45.037Z" }, + { url = "https://files.pythonhosted.org/packages/86/a1/142de946dc4393f910bf4564b5c3ba819906e1f49b06c9cb557519c849e4/time_machine-3.2.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4e374779021446fc2b5c29d80457ec9a3b1a5df043dc2aae07d7c1415d52323c", size = 19991, upload-time = "2025-12-17T23:32:49.933Z" }, + { url = "https://files.pythonhosted.org/packages/ee/62/7f17def6289901f94726921811a16b9adce46e666362c75d45730c60274f/time_machine-3.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:122310a6af9c36e9a636da32830e591e7923e8a07bdd0a43276c3a36c6821c90", size = 15707, upload-time = "2025-12-17T23:32:50.969Z" }, + { url = "https://files.pythonhosted.org/packages/5d/d3/3502fb9bd3acb159c18844b26c43220201a0d4a622c0c853785d07699a92/time_machine-3.2.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ba3eeb0f018cc362dd8128befa3426696a2e16dd223c3fb695fde184892d4d8c", size = 39207, upload-time = "2025-12-17T23:32:52.033Z" }, + { url = "https://files.pythonhosted.org/packages/5a/be/8b27f4aa296fda14a5a2ad7f588ddd450603c33415ab3f8e85b2f1a44678/time_machine-3.2.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:77d38ba664b381a7793f8786efc13b5004f0d5f672dae814430445b8202a67a6", size = 40764, upload-time = "2025-12-17T23:32:53.167Z" }, + { url = "https://files.pythonhosted.org/packages/42/cd/fe4c4e5c8ab6d48fab3624c32be9116fb120173a35fe67e482e5cf68b3d2/time_machine-3.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f09abeb8f03f044d72712207e0489a62098ad3ad16dac38927fcf80baca4d6a7", size = 43508, upload-time = "2025-12-17T23:32:54.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/28/5a3ba2fce85b97655a425d6bb20a441550acd2b304c96b2c19d3839f721a/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6b28367ce4f73987a55e230e1d30a57a3af85da8eb1a140074eb6e8c7e6ef19f", size = 41712, upload-time = "2025-12-17T23:32:55.781Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/e38084be7fdabb4835db68a3a47e58c34182d79fc35df1ecbe0db2c5359f/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:903c7751c904581da9f7861c3015bed7cdc40047321291d3694a3cdc783bbca3", size = 38939, upload-time = "2025-12-17T23:32:56.867Z" }, + { url = "https://files.pythonhosted.org/packages/40/d0/ad3feb0a392ef4e0c08bc32024950373ddc0669002cbdcbb9f3bf0c2d114/time_machine-3.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:528217cad85ede5f85c8bc78b0341868d3c3cfefc6ecb5b622e1cacb6c73247b", size = 39837, upload-time = "2025-12-17T23:32:58.283Z" }, +] + [[package]] name = "tinytag" version = "2.2.1" @@ -5474,18 +5553,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/eb/d5583a11486211f3ebd4b385545ae787f32363d453c19fffd81106c9c138/whitenoise-6.12.0-py3-none-any.whl", hash = "sha256:fc5e8c572e33ebf24795b47b6a7da8da3c00cff2349f5b04c02f28d0cc5a3cc2", size = 20302, upload-time = "2026-02-27T00:05:40.086Z" }, ] -[[package]] -name = "whoosh-reloaded" -version = "2.7.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cached-property", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/17/51/3fb4b9fdeaaf96512514ccf2871186333ce41a0de2ea48236a4056a5f6af/Whoosh-Reloaded-2.7.5.tar.gz", hash = "sha256:39ed7dfbd1fec97af33933107bdf78110728375ed0f2abb25dec6dbfdcb279d8", size = 1061606, upload-time = "2024-02-02T20:06:42.285Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/90/866dfe421f188217ecd7339585e961034a7f4fdc96b62cec3b40a50dbdef/Whoosh_Reloaded-2.7.5-py2.py3-none-any.whl", hash = "sha256:2ab6aeeafb359fbff4beb3c704b960fd88240354f3363f1c5bdb5c2325cae80e", size = 551793, upload-time = "2024-02-02T20:06:39.868Z" }, -] - [[package]] name = "wrapt" version = "2.0.1"