From 17499cc10905a26e49de8a0aa6531bea1995beb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20=C5=A0iagi?= Date: Fri, 25 Sep 2026 13:05:09 +0200 Subject: [PATCH] feat: add meilisearch for schools --- compose.yml | 6 + pyproject.toml | 1 + .../management/commands/indexschools.py | 56 +++++++++ .../management/commands/syncschools.py | 4 + .../templates/settings/school_search.html | 2 +- trojstenid/schools/views.py | 9 +- trojstenid/search.py | 78 +++++++++++++ trojstenid/settings.py | 2 + trojstenid/users/views_api.py | 2 +- uv.lock | 108 +++++++++++++++++- 10 files changed, 262 insertions(+), 6 deletions(-) create mode 100644 trojstenid/schools/management/commands/indexschools.py create mode 100644 trojstenid/search.py diff --git a/compose.yml b/compose.yml index 8066254..57c22b5 100644 --- a/compose.yml +++ b/compose.yml @@ -36,5 +36,11 @@ services: redis: image: redis + meilisearch: + image: getmeili/meilisearch:v1.12.5 + volumes: + - search_data:/meili_data + volumes: postgres_data: + search_data: diff --git a/pyproject.toml b/pyproject.toml index 17587f0..8fbf378 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,6 +30,7 @@ dependencies = [ "google-api-python-client>=2.196.0", "google-auth>=2.53.0", "pygithub>=2.9.1", + "meilisearch>=0.43.0", ] [tool.ruff.lint] diff --git a/trojstenid/schools/management/commands/indexschools.py b/trojstenid/schools/management/commands/indexschools.py new file mode 100644 index 0000000..2b84cdb --- /dev/null +++ b/trojstenid/schools/management/commands/indexschools.py @@ -0,0 +1,56 @@ +from django.core.management.base import BaseCommand + +from trojstenid import search +from trojstenid.schools.models import School + + +class Command(BaseCommand): + help = "Send schools to Meilisearch for indexing" + + def handle(self, *args, **options): + self.stdout.write("Indexing schools...") + + schools = [] + for s in School.objects.filter(is_selectable=True).all(): + schools.append(s.to_dict()) + + search.client.index("schools").update_settings( + { + "filterableAttributes": ["types"], + "sortableAttributes": ["name", "address"], + "rankingRules": [ + "words", + "sort", + "typo", + "proximity", + "attribute", + "exactness", + ], + "synonyms": { + "zš": [ + "základná škola", + "základní škola", + "elemi iskola", + "alapiskola", + ], + "základná škola": ["zš"], + "základní škola": ["zš"], + "elemi iskola": ["zš"], + "alapiskola": ["zš"], + "sš": [ + "stredná škola", + "strední škola", + "középiskola", + "střední škola", + ], + "stredná škola": ["sš"], + "strední škola": ["sš"], + "középiskola": ["sš"], + "střední škola": ["sš"], + }, + } + ) + + search.client.index("schools").add_documents(schools) + + self.stdout.write(self.style.SUCCESS("Schools indexed successfully.")) diff --git a/trojstenid/schools/management/commands/syncschools.py b/trojstenid/schools/management/commands/syncschools.py index 54f54c7..f697d9d 100644 --- a/trojstenid/schools/management/commands/syncschools.py +++ b/trojstenid/schools/management/commands/syncschools.py @@ -4,6 +4,8 @@ from trojstenid.schools.services.school_sync import create_null_school, sync_schools +from .indexschools import Command as IndexSchoolsCommand + class Command(BaseCommand): help = "Sync school database." @@ -12,3 +14,5 @@ def handle(self, *args: Any, **options: Any) -> str | None: sync_schools() create_null_school() self.stdout.write(self.style.SUCCESS("School sync sucessfull.")) + + IndexSchoolsCommand().handle() diff --git a/trojstenid/schools/templates/settings/school_search.html b/trojstenid/schools/templates/settings/school_search.html index b0b46da..944b228 100644 --- a/trojstenid/schools/templates/settings/school_search.html +++ b/trojstenid/schools/templates/settings/school_search.html @@ -1,5 +1,5 @@ {% for school in schools %} -{{ school }} +{{ school.name }}{% if school.address %}, {{ school.address }}{% endif %} {% endfor %} Moja škola nie je v zozname. diff --git a/trojstenid/schools/views.py b/trojstenid/schools/views.py index 40e5f8b..d3c0198 100644 --- a/trojstenid/schools/views.py +++ b/trojstenid/schools/views.py @@ -11,6 +11,7 @@ from django.views import View from django.views.generic import FormView, ListView, TemplateView +from trojstenid import search from trojstenid.schools.forms import SchoolRecordForm from trojstenid.schools.models import School, UserSchoolRecord from trojstenid.users.models import User @@ -111,5 +112,11 @@ class SchoolSearchView(LoginRequiredMixin, TemplateView): def get_context_data(self, **kwargs: Any) -> dict[str, Any]: ctx = super().get_context_data(**kwargs) query = self.request.GET.get("q", "") - ctx["schools"] = School.objects.search(query)[:10] + result = search.client.index("schools").search( + query, + { + "limit": 10, + }, + ) + ctx["schools"] = result["hits"] return ctx diff --git a/trojstenid/search.py b/trojstenid/search.py new file mode 100644 index 0000000..eb3acbe --- /dev/null +++ b/trojstenid/search.py @@ -0,0 +1,78 @@ +from functools import cache + +import meilisearch +from django.conf import settings +from django.core.paginator import Page + +enabled = bool(settings.MEILISEARCH_URL) +client = meilisearch.Client(settings.MEILISEARCH_URL, settings.MEILISEARCH_KEY) + + +class MeiliQuerySet: + """ + MeiliQuerySet provides a Paginator-compatible API to allow the use of Meilisearch + results in ListViews. + + When using our admin_paginator, it is advisable to use `nerf_page()` on the Page + object. + """ + + def __init__(self, queryset, index, search_term, opt_params=None): + self.queryset = queryset + self.index = index + self.search_term = search_term + self.opt_params = opt_params + + @cache + def get_results(self, offset=0, limit=20): + params = self.opt_params or {} + params["offset"] = offset + params["limit"] = limit + + res = client.index(self.index).search(self.search_term, params) + + hits = res["hits"] + ids = [hit["id"] for hit in hits] + objects = self.queryset.filter(id__in=ids) + objects = sorted(objects, key=lambda o: ids.index(o.id)) + return objects + + def __len__(self): + return 10**10 + + def __getitem__(self, item): + if not isinstance(item, slice): + raise ValueError( + "Instance of MeiliQuerySet does not support getting exact items." + ) + + return self.get_results(item.start, item.stop - item.start) + + +class DumbPage(Page): + """ + A dumb implementation of Page object that does not need to know the + total number of pages. + """ + + def __repr__(self): + return f"" + + def __len__(self): + return len(self.object_list) + + def has_next(self): + return len(self) == self.paginator.per_page + + def next_page_number(self): + return self.number + 1 + + def previous_page_number(self): + return self.number - 1 + + +def nerf_page(page: Page) -> DumbPage: + """ + Converts a normal Page object into a dumb one. + """ + return DumbPage(page.object_list, page.number, page.paginator) diff --git a/trojstenid/settings.py b/trojstenid/settings.py index fdd4a7a..1da9e00 100644 --- a/trojstenid/settings.py +++ b/trojstenid/settings.py @@ -287,6 +287,8 @@ }, } +MEILISEARCH_URL = env("MEILISEARCH_URL", default="http://meilisearch:7700/") +MEILISEARCH_KEY = env("MEILISEARCH_KEY", default=None) if DEBUG: import socket # only if you haven't already imported this diff --git a/trojstenid/users/views_api.py b/trojstenid/users/views_api.py index e6038ec..19347cb 100644 --- a/trojstenid/users/views_api.py +++ b/trojstenid/users/views_api.py @@ -97,5 +97,5 @@ def get_object(self): try: return qs.get(id=int(lookup_value)) - except (ValueError, User.DoesNotExist): + except ValueError, User.DoesNotExist: return qs.get(username=lookup_value) diff --git a/uv.lock b/uv.lock index dc9df6c..1d287dd 100644 --- a/uv.lock +++ b/uv.lock @@ -2,9 +2,14 @@ version = 1 revision = 3 requires-python = "==3.14.*" -[options] -exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. -exclude-newer-span = "P7D" +[[package]] +name = "annotated-types" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, +] [[package]] name = "asgiref" @@ -30,6 +35,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/09/34/57d038ae30374976ce4ec57db9dea95bf55d1b5543b35e77aa9ce3543198/bumpver-2024.1130-py2.py3-none-any.whl", hash = "sha256:8e54220aefe7db25148622f45959f7beb6b8513af0b0429b38b9072566665a49", size = 65273, upload-time = "2024-11-10T20:51:50.383Z" }, ] +[[package]] +name = "camel-converter" +version = "5.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6f/fb/58f39ce79ec56e480ac86e4c6a7758d2ee7503c7e9c6f91f535dcbae81da/camel_converter-5.1.1.tar.gz", hash = "sha256:fd601b343695025e9d4ec316845d1fe6011cd2ded763397032feb1ef74855b7e", size = 50295, upload-time = "2026-08-25T01:03:26.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c9/9f/fcfe35d042eb4405d158aaba613dc712f18350c5e6ea3e2376c8a8dcbe10/camel_converter-5.1.1-py3-none-any.whl", hash = "sha256:7cf028484efd7cf0200a05812307e97e83f161af256552e8dfe5833150bab21c", size = 6968, upload-time = "2026-08-25T01:03:25.416Z" }, +] + +[package.optional-dependencies] +pydantic = [ + { name = "pydantic" }, +] + [[package]] name = "certifi" version = "2026.7.22" @@ -642,6 +661,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/43/57/4526ca0e214a3d158690e3393f6d547a2f8070f88d6388ab21d5b0aac8a1/marshmallow-4.3.1-py3-none-any.whl", hash = "sha256:e65accfbe277546df92ed7996a678c90e063e9a7c2a2f5e03f7d0b90e3768c42", size = 49219, upload-time = "2026-08-08T14:27:28.078Z" }, ] +[[package]] +name = "meilisearch" +version = "0.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "camel-converter", extra = ["pydantic"] }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/98/0c0231e407683304374c26c41f225bc80424afd1944733df0d9e4bbe8e02/meilisearch-0.43.0.tar.gz", hash = "sha256:c3f578a54070e0b545f081261fe1ee47a73ae022b17d230b949d889f0d9a7e7e", size = 33980, upload-time = "2026-07-22T08:12:29.38Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/70/cd4e0773eb4bcb22cc1f4d6b43d487f4a20fc515dbf1078e76a67e514c75/meilisearch-0.43.0-py3-none-any.whl", hash = "sha256:4226734a510211f0183c2f16af7b6077d239f0c4ec310a58ef564ae704cabe26", size = 34638, upload-time = "2026-07-22T08:12:28.325Z" }, +] + [[package]] name = "nodeenv" version = "1.10.0" @@ -825,6 +857,62 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, ] +[[package]] +name = "pydantic" +version = "2.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" }, + { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" }, + { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" }, + { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" }, + { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" }, + { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" }, + { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" }, + { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" }, + { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" }, + { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" }, + { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" }, + { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" }, +] + [[package]] name = "pygithub" version = "2.10.0" @@ -1077,6 +1165,7 @@ dependencies = [ { name = "google-api-python-client" }, { name = "google-auth" }, { name = "gunicorn" }, + { name = "meilisearch" }, { name = "pillow" }, { name = "psycopg", extra = ["binary"] }, { name = "pygithub" }, @@ -1111,6 +1200,7 @@ requires-dist = [ { name = "google-api-python-client", specifier = ">=2.196.0" }, { name = "google-auth", specifier = ">=2.53.0" }, { name = "gunicorn", specifier = "~=23.0.0" }, + { name = "meilisearch", specifier = ">=0.43.0" }, { name = "pillow", specifier = "~=12.3.0" }, { name = "psycopg", extras = ["binary"], specifier = "~=3.2.7" }, { name = "pygithub", specifier = ">=2.9.1" }, @@ -1152,6 +1242,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] +[[package]] +name = "typing-inspection" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, +] + [[package]] name = "tzdata" version = "2026.3"