From e8830e846a40a6b4484b4c07d7ec752ca0faca87 Mon Sep 17 00:00:00 2001 From: snw Date: Wed, 23 Sep 2026 23:52:35 +0000 Subject: [PATCH] Build Flet Material component gallery --- .gitignore | 9 + .python-version | 2 + README.md | 50 ++++ pyproject.toml | 26 +++ src/gallery/__init__.py | 2 + src/gallery/app.py | 231 +++++++++++++++++++ src/gallery/examples/__init__.py | 9 + src/gallery/examples/actions.py | 68 ++++++ src/gallery/examples/content.py | 88 +++++++ src/gallery/examples/data_feedback.py | 71 ++++++ src/gallery/examples/inputs.py | 81 +++++++ src/gallery/examples/layout.py | 60 +++++ src/gallery/examples/navigation.py | 48 ++++ src/gallery/models.py | 21 ++ src/gallery/source_links.py | 20 ++ src/gallery/source_manifest.json | 137 +++++++++++ src/main.py | 6 + tests/test_gallery.py | 27 +++ tools/generate_source_manifest.py | 52 +++++ uv.lock | 319 ++++++++++++++++++++++++++ 20 files changed, 1327 insertions(+) create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 README.md create mode 100644 pyproject.toml create mode 100644 src/gallery/__init__.py create mode 100644 src/gallery/app.py create mode 100644 src/gallery/examples/__init__.py create mode 100644 src/gallery/examples/actions.py create mode 100644 src/gallery/examples/content.py create mode 100644 src/gallery/examples/data_feedback.py create mode 100644 src/gallery/examples/inputs.py create mode 100644 src/gallery/examples/layout.py create mode 100644 src/gallery/examples/navigation.py create mode 100644 src/gallery/models.py create mode 100644 src/gallery/source_links.py create mode 100644 src/gallery/source_manifest.json create mode 100644 src/main.py create mode 100644 tests/test_gallery.py create mode 100644 tools/generate_source_manifest.py create mode 100644 uv.lock diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8e560a1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.venv/ +__pycache__/ +*.py[cod] +.pytest_cache/ +.ruff_cache/ +build/ +dist/ +.DS_Store + diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..ebe773d --- /dev/null +++ b/.python-version @@ -0,0 +1,2 @@ +3.13 + diff --git a/README.md b/README.md new file mode 100644 index 0000000..4a39a80 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# Flet Component Gallery + +Интерактивная галерея компонентов Flet в духе Google Compose Material Catalog. +Каждый пример — рабочий интерфейс и отдельная читаемая Python-функция; кнопка +`` рядом с примером открывает точную строку исходного кода в репозитории. + +В каталоге 27 интерактивных примеров: кнопки, поля ввода, controls выбора, +типографика, карточки, списки, адаптивные layouts, навигация, таблицы, progress, +dialogs, snackbar и bottom sheet. Постоянная кнопка настроек позволяет менять +светлую/тёмную/системную тему, Material 3 accent color и видимость кнопок кода. + +## Запуск + +```bash +uv sync +uv run flet run src/main.py +``` + +В браузере: + +```bash +uv run flet run --web src/main.py +``` + +## Проверки и ссылки на код + +```bash +uv run python tools/generate_source_manifest.py +uv run ruff check . +uv run pytest -q +``` + +Генератор разбирает учебные функции через AST и записывает точные строки в +`src/gallery/source_manifest.json`. Манифест следует обновлять перед коммитом и +сборкой. + +## Android ARMv8 + +ARMv8 в Android соответствует ABI `arm64-v8a`: + +```bash +uv run flet build apk src --arch arm64-v8a --yes \ + --project flet_component_gallery \ + --product "Flet Material Catalog" \ + --org su.snw +``` + +APK появится в `src/build/apk/`. Платформенные controls, требующие permissions, +нативных сервисов или внешних расширений (камера, карта, WebView), намеренно не +имитируются: каталог сфокусирован на стандартных визуальных Flet controls. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..21a8075 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,26 @@ +[project] +name = "flet-component-gallery" +version = "0.1.0" +description = "Composer-inspired interactive gallery of Flet controls" +readme = "README.md" +requires-python = ">=3.11" +dependencies = [ + "flet==1.0.1", +] + +[dependency-groups] +dev = [ + "pytest>=8.4.0", + "ruff>=0.13.0", +] + +[tool.uv] +package = false + +[tool.ruff] +line-length = 100 +target-version = "py311" + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["src"] diff --git a/src/gallery/__init__.py b/src/gallery/__init__.py new file mode 100644 index 0000000..77b0169 --- /dev/null +++ b/src/gallery/__init__.py @@ -0,0 +1,2 @@ +"""Flet Component Gallery package.""" + diff --git a/src/gallery/app.py b/src/gallery/app.py new file mode 100644 index 0000000..ae03f11 --- /dev/null +++ b/src/gallery/app.py @@ -0,0 +1,231 @@ +import flet as ft + +from gallery.examples import ALL_EXAMPLES +from gallery.models import Example, GallerySettings +from gallery.source_links import source_url + +SEED_COLORS = { + "Фиолетовый": "#6750A4", + "Синий": "#0061A4", + "Зелёный": "#006D3B", + "Оранжевый": "#8B5000", + "Розовый": "#984061", +} + + +class GalleryApp: + def __init__(self, page: ft.Page) -> None: + self.page = page + self.settings = GallerySettings() + self.selected_category = "Обзор" + self.query = "" + + @property + def categories(self) -> list[str]: + return ["Обзор", *dict.fromkeys(example.category for example in ALL_EXAMPLES)] + + def apply_theme(self) -> None: + self.page.theme_mode = self.settings.theme_mode + self.page.theme = ft.Theme(color_scheme_seed=self.settings.seed_color, use_material3=True) + self.page.dark_theme = ft.Theme(color_scheme_seed=self.settings.seed_color, use_material3=True) + + def settings_dialog(self) -> ft.AlertDialog: + theme_group = ft.RadioGroup( + value=self.settings.theme_mode.value, + content=ft.Column( + controls=[ + ft.Radio(value="system", label="Как в системе"), + ft.Radio(value="light", label="Светлая"), + ft.Radio(value="dark", label="Тёмная"), + ], + tight=True, + ), + ) + color_group = ft.RadioGroup( + value=self.settings.seed_color, + content=ft.Row( + controls=[ + ft.Radio(value=color, label=name, fill_color=color) + for name, color in SEED_COLORS.items() + ], + spacing=8, + wrap=True, + ), + ) + source_toggle = ft.Switch( + label="Показывать кнопки исходного кода", + value=self.settings.show_source_buttons, + ) + + def save(e: ft.Event[ft.FilledButton]) -> None: + self.settings.theme_mode = ft.ThemeMode(theme_group.value) + self.settings.seed_color = color_group.value + self.settings.show_source_buttons = source_toggle.value + self.page.pop_dialog() + self.apply_theme() + self.render() + + return ft.AlertDialog( + icon=ft.Icon(ft.Icons.PALETTE_OUTLINED), + title="Внешний вид каталога", + content=ft.Container( + content=ft.Column( + controls=[ + ft.Text("Тема", theme_style=ft.TextThemeStyle.TITLE_MEDIUM), + theme_group, + ft.Divider(), + ft.Text("Акцентный цвет", theme_style=ft.TextThemeStyle.TITLE_MEDIUM), + color_group, + ft.Divider(), + source_toggle, + ], + tight=True, + scroll=ft.ScrollMode.AUTO, + ), + width=460, + height=520, + ), + actions=[ + ft.TextButton("Отмена", on_click=lambda e: self.page.pop_dialog()), + ft.FilledButton("Применить", on_click=save), + ], + ) + + def open_settings(self, _=None) -> None: + self.page.show_dialog(self.settings_dialog()) + + def example_card(self, example: Example) -> ft.Control: + actions: list[ft.Control] = [] + if self.settings.show_source_buttons: + actions.append( + ft.IconButton( + icon=ft.Icons.CODE, + tooltip="Показать код", + url=source_url(example.id), + ) + ) + return ft.Card( + content=ft.Container( + content=ft.Column( + controls=[ + ft.Row( + controls=[ + ft.Column( + controls=[ + ft.Text(example.title, theme_style=ft.TextThemeStyle.TITLE_LARGE), + ft.Text(example.description, color=ft.Colors.ON_SURFACE_VARIANT), + ], + spacing=2, + expand=True, + ), + *actions, + ], + vertical_alignment=ft.CrossAxisAlignment.START, + ), + ft.Divider(), + ft.Container(content=example.builder(), padding=ft.Padding.only(top=8)), + ], + spacing=12, + ), + padding=20, + ), + margin=ft.Margin.only(bottom=12), + ) + + def current_examples(self) -> list[Example]: + examples = ALL_EXAMPLES + if self.selected_category != "Обзор": + examples = [e for e in examples if e.category == self.selected_category] + query = self.query.casefold().strip() + if query: + examples = [ + e + for e in examples + if query in f"{e.title} {e.description} {e.category}".casefold() + ] + return examples + + def header(self) -> ft.Control: + count = len(self.current_examples()) + return ft.Column( + controls=[ + ft.Text(self.selected_category, theme_style=ft.TextThemeStyle.DISPLAY_SMALL), + ft.Text( + f"{count} интерактивных примеров · Flet 1.0.1", + color=ft.Colors.ON_SURFACE_VARIANT, + ), + ], + spacing=4, + ) + + def navigation(self) -> ft.Control: + icons = [ + ft.Icons.HOME_OUTLINED, + ft.Icons.TOUCH_APP_OUTLINED, + ft.Icons.TUNE, + ft.Icons.ARTICLE_OUTLINED, + ft.Icons.GRID_VIEW, + ft.Icons.NAVIGATION_OUTLINED, + ft.Icons.DATA_OBJECT, + ] + + def select(e: ft.Event[ft.NavigationRail]) -> None: + self.selected_category = self.categories[e.control.selected_index] + self.render() + + return ft.NavigationRail( + selected_index=self.categories.index(self.selected_category), + destinations=[ + ft.NavigationRailDestination(icon=icon, label=category) + for category, icon in zip(self.categories, icons, strict=True) + ], + label_type=ft.NavigationRailLabelType.ALL, + on_change=select, + min_width=96, + ) + + def render(self) -> None: + self.page.clean() + self.page.title = "Flet Component Gallery" + self.page.padding = 0 + self.apply_theme() + + def search(e: ft.Event[ft.TextField]) -> None: + self.query = e.control.value or "" + self.render() + + body = ft.ListView( + controls=[ + self.header(), + ft.TextField( + value=self.query, + hint_text="Найти компонент…", + prefix_icon=ft.Icons.SEARCH, + on_submit=search, + suffix=ft.IconButton(ft.Icons.CLOSE, tooltip="Сбросить", on_click=lambda e: self.clear_search()), + ), + *[self.example_card(example) for example in self.current_examples()], + ft.Container(height=24), + ], + expand=True, + padding=ft.Padding.symmetric(horizontal=24, vertical=20), + spacing=16, + ) + self.page.appbar = ft.AppBar( + leading=ft.Icon(ft.Icons.WIDGETS_OUTLINED), + title=ft.Text("Flet Material Catalog"), + actions=[ + ft.IconButton(ft.Icons.SETTINGS_OUTLINED, tooltip="Настройки", on_click=self.open_settings), + ft.IconButton(ft.Icons.OPEN_IN_NEW, tooltip="Репозиторий", url="https://git.snw.su/snw/FletAppDemo"), + ], + ) + self.page.add(ft.Row([self.navigation(), ft.VerticalDivider(width=1), body], expand=True, spacing=0)) + self.page.update() + + def clear_search(self) -> None: + self.query = "" + self.render() + + +def main(page: ft.Page) -> None: + GalleryApp(page).render() diff --git a/src/gallery/examples/__init__.py b/src/gallery/examples/__init__.py new file mode 100644 index 0000000..0ee9b1d --- /dev/null +++ b/src/gallery/examples/__init__.py @@ -0,0 +1,9 @@ +from gallery.examples.actions import EXAMPLES as ACTIONS +from gallery.examples.content import EXAMPLES as CONTENT +from gallery.examples.data_feedback import EXAMPLES as DATA_FEEDBACK +from gallery.examples.inputs import EXAMPLES as INPUTS +from gallery.examples.layout import EXAMPLES as LAYOUT +from gallery.examples.navigation import EXAMPLES as NAVIGATION + +ALL_EXAMPLES = ACTIONS + INPUTS + CONTENT + LAYOUT + NAVIGATION + DATA_FEEDBACK + diff --git a/src/gallery/examples/actions.py b/src/gallery/examples/actions.py new file mode 100644 index 0000000..7335cce --- /dev/null +++ b/src/gallery/examples/actions.py @@ -0,0 +1,68 @@ +import flet as ft + +from gallery.models import Example + + +def filled_buttons() -> ft.Control: + return ft.Row( + controls=[ + ft.FilledButton("Обычная", on_click=lambda e: None), + ft.FilledButton("С иконкой", icon=ft.Icons.ADD, on_click=lambda e: None), + ft.FilledButton("Отключена", disabled=True), + ], + spacing=12, + run_spacing=12, + wrap=True, + ) + + +def outlined_and_text_buttons() -> ft.Control: + return ft.Row( + controls=[ + ft.OutlinedButton("Outlined", icon=ft.Icons.FAVORITE_BORDER), + ft.TextButton("Text button"), + ft.TextButton("Отключена", disabled=True), + ], + spacing=12, + wrap=True, + ) + + +def icon_buttons() -> ft.Control: + return ft.Row( + controls=[ + ft.IconButton(ft.Icons.FAVORITE, tooltip="Избранное"), + ft.IconButton(ft.Icons.SHARE, tooltip="Поделиться"), + ft.IconButton(ft.Icons.DELETE_OUTLINE, tooltip="Удалить", disabled=True), + ] + ) + + +def floating_action_button() -> ft.Control: + return ft.Row( + controls=[ + ft.FloatingActionButton(icon=ft.Icons.ADD, tooltip="Создать"), + ft.FloatingActionButton(content="Создать", icon=ft.Icons.EDIT), + ] + ) + + +def chips() -> ft.Control: + return ft.Row( + controls=[ + ft.Chip("Python", leading=ft.Icon(ft.Icons.CODE), selected=True), + ft.Chip("Flet", leading=ft.Icon(ft.Icons.WIDGETS)), + ft.Chip("Удаляемый", delete_icon=ft.Icon(ft.Icons.CLOSE)), + ], + spacing=8, + wrap=True, + ) + + +EXAMPLES = [ + Example("filled-buttons", "Filled buttons", "Основные кнопки и их состояния.", "Действия", filled_buttons), + Example("outlined-buttons", "Outlined и text", "Кнопки с меньшим визуальным весом.", "Действия", outlined_and_text_buttons), + Example("icon-buttons", "Icon buttons", "Компактные действия с подсказками.", "Действия", icon_buttons), + Example("fab", "Floating action button", "Главное действие экрана.", "Действия", floating_action_button), + Example("chips", "Chips", "Фильтры, теги и компактный выбор.", "Действия", chips), +] diff --git a/src/gallery/examples/content.py b/src/gallery/examples/content.py new file mode 100644 index 0000000..ec9a46b --- /dev/null +++ b/src/gallery/examples/content.py @@ -0,0 +1,88 @@ +import flet as ft + +from gallery.models import Example + + +def typography() -> ft.Control: + return ft.Column( + controls=[ + ft.Text("Display small", theme_style=ft.TextThemeStyle.DISPLAY_SMALL), + ft.Text("Headline medium", theme_style=ft.TextThemeStyle.HEADLINE_MEDIUM), + ft.Text("Title large", theme_style=ft.TextThemeStyle.TITLE_LARGE), + ft.Text("Body medium — основной текст интерфейса."), + ft.Text("Label small", theme_style=ft.TextThemeStyle.LABEL_SMALL), + ], + spacing=8, + ) + + +def icons_and_badges() -> ft.Control: + return ft.Row( + controls=[ + ft.Icon(ft.Icons.HOME, size=32), + ft.Icon(ft.Icons.FAVORITE, color=ft.Colors.ERROR, size=32), + ft.IconButton(ft.Icons.NOTIFICATIONS, badge=ft.Badge("3")), + ft.CircleAvatar(content="FL", bgcolor=ft.Colors.PRIMARY_CONTAINER), + ], + spacing=20, + ) + + +def cards() -> ft.Control: + return ft.ResponsiveRow( + controls=[ + ft.Card( + content=ft.Container( + content=ft.Column( + controls=[ + ft.Icon(ft.Icons.ROCKET_LAUNCH, size=36), + ft.Text("Карточка", theme_style=ft.TextThemeStyle.TITLE_MEDIUM), + ft.Text("Связывает содержимое в одну смысловую группу."), + ] + ), + padding=20, + ), + col={"sm": 12, "md": 6}, + ), + ft.Card( + content=ft.ListTile( + leading=ft.Icons.ALBUM, + title="ListTile внутри Card", + subtitle="Компактное представление данных", + trailing=ft.Icons.MORE_VERT, + ), + col={"sm": 12, "md": 6}, + ), + ] + ) + + +def lists_and_expansion() -> ft.Control: + return ft.Column( + controls=[ + ft.ListTile(leading=ft.Icons.PERSON, title="Ada Lovelace", subtitle="admin@example.dev", trailing=ft.Icons.CHEVRON_RIGHT), + ft.ExpansionTile( + title="Подробнее", + leading=ft.Icons.INFO_OUTLINE, + controls=[ft.Container(ft.Text("Скрытое содержимое раскрывающегося блока."), padding=16)], + ), + ] + ) + + +def markdown() -> ft.Control: + return ft.Markdown( + "## Markdown\n\nFlet поддерживает **форматирование**, списки и `inline code`.\n\n- быстро\n- понятно\n- кроссплатформенно", + selectable=True, + extension_set=ft.MarkdownExtensionSet.GITHUB_WEB, + ) + + +EXAMPLES = [ + Example("typography", "Typography", "Иерархия текстовых стилей темы.", "Контент", typography), + Example("icons-badges", "Icons, badges и avatar", "Иконки, счётчики и идентичность.", "Контент", icons_and_badges), + Example("cards", "Cards", "Поверхности для связанных данных.", "Контент", cards), + Example("lists", "Lists и expansion", "Строки списков и раскрытие деталей.", "Контент", lists_and_expansion), + Example("markdown", "Markdown", "Форматированный технический контент.", "Контент", markdown), +] + diff --git a/src/gallery/examples/data_feedback.py b/src/gallery/examples/data_feedback.py new file mode 100644 index 0000000..576519a --- /dev/null +++ b/src/gallery/examples/data_feedback.py @@ -0,0 +1,71 @@ +import flet as ft + +from gallery.models import Example + + +def data_table() -> ft.Control: + return ft.DataTable( + columns=[ft.DataColumn(ft.Text("Компонент")), ft.DataColumn(ft.Text("Статус")), ft.DataColumn(ft.Text("Платформы"))], + rows=[ + ft.DataRow(cells=[ft.DataCell(ft.Text("Button")), ft.DataCell(ft.Text("Stable")), ft.DataCell(ft.Text("All"))]), + ft.DataRow(cells=[ft.DataCell(ft.Text("Camera")), ft.DataCell(ft.Text("Service")), ft.DataCell(ft.Text("Mobile"))]), + ft.DataRow(cells=[ft.DataCell(ft.Text("WebView")), ft.DataCell(ft.Text("Extension")), ft.DataCell(ft.Text("Varies"))]), + ], + ) + + +def progress_indicators() -> ft.Control: + return ft.Column( + controls=[ + ft.Text("Determinate"), + ft.ProgressBar(value=0.65, border_radius=8), + ft.Row([ft.ProgressRing(value=0.72), ft.ProgressRing(), ft.Text("72% и бесконечная загрузка")], spacing=16), + ], + spacing=12, + ) + + +def alert_dialog() -> ft.Control: + def show(e: ft.Event[ft.FilledButton]) -> None: + e.control.page.show_dialog( + ft.AlertDialog( + icon=ft.Icon(ft.Icons.DELETE_OUTLINE), + title="Удалить проект?", + content=ft.Text("Это действие показано только для демонстрации."), + actions=[ft.TextButton("Отмена", on_click=lambda event: event.control.page.pop_dialog()), ft.FilledButton("Удалить", on_click=lambda event: event.control.page.pop_dialog())], + ) + ) + + return ft.FilledButton("Открыть диалог", icon=ft.Icons.OPEN_IN_NEW, on_click=show) + + +def snackbar() -> ft.Control: + def show(e: ft.Event[ft.FilledButton]) -> None: + e.control.page.show_dialog(ft.SnackBar("Изменения сохранены", show_close_icon=True)) + + return ft.FilledButton("Показать snackbar", icon=ft.Icons.CHECK_CIRCLE, on_click=show) + + +def bottom_sheet() -> ft.Control: + def show(e: ft.Event[ft.FilledButton]) -> None: + e.control.page.show_dialog( + ft.BottomSheet( + content=ft.Container( + content=ft.Column([ft.Text("Bottom sheet", theme_style=ft.TextThemeStyle.HEADLINE_SMALL), ft.Text("Подходит для дополнительных действий на мобильном экране.")], tight=True), + padding=24, + ), + show_drag_handle=True, + ) + ) + + return ft.FilledButton("Открыть bottom sheet", icon=ft.Icons.VERTICAL_ALIGN_TOP, on_click=show) + + +EXAMPLES = [ + Example("data-table", "DataTable", "Табличные структурированные данные.", "Данные и feedback", data_table), + Example("progress", "Progress indicators", "Определённая и бесконечная загрузка.", "Данные и feedback", progress_indicators), + Example("dialog", "AlertDialog", "Модальное подтверждение важного действия.", "Данные и feedback", alert_dialog), + Example("snackbar", "SnackBar", "Краткое неблокирующее сообщение.", "Данные и feedback", snackbar), + Example("bottom-sheet", "BottomSheet", "Дополнительный мобильный слой.", "Данные и feedback", bottom_sheet), +] + diff --git a/src/gallery/examples/inputs.py b/src/gallery/examples/inputs.py new file mode 100644 index 0000000..03927e7 --- /dev/null +++ b/src/gallery/examples/inputs.py @@ -0,0 +1,81 @@ +import flet as ft + +from gallery.models import Example + + +def text_fields() -> ft.Control: + return ft.Column( + controls=[ + ft.TextField(label="Имя", hint_text="Алексей", prefix_icon=ft.Icons.PERSON), + ft.TextField(label="Пароль", password=True, can_reveal_password=True), + ft.TextField(label="Комментарий", multiline=True, min_lines=2, max_lines=4), + ], + spacing=12, + ) + + +def dropdowns() -> ft.Control: + return ft.Dropdown( + label="Среда", + value="web", + options=[ + ft.DropdownOption(key="web", text="Web"), + ft.DropdownOption(key="android", text="Android"), + ft.DropdownOption(key="desktop", text="Desktop"), + ], + width=300, + ) + + +def selection_controls() -> ft.Control: + return ft.Column( + controls=[ + ft.Checkbox("Получать обновления", value=True), + ft.Switch(label="Автоматическая тема", value=False), + ft.RadioGroup( + value="comfortable", + content=ft.Row( + controls=[ + ft.Radio(value="compact", label="Компактно"), + ft.Radio(value="comfortable", label="Удобно"), + ] + ), + ), + ] + ) + + +def sliders() -> ft.Control: + value_label = ft.Text("Громкость: 40") + + def changed(e: ft.Event[ft.Slider]) -> None: + value_label.value = f"Громкость: {int(e.control.value)}" + value_label.update() + + return ft.Column( + controls=[ + value_label, + ft.Slider(value=40, min=0, max=100, divisions=10, label="{value}", on_change=changed), + ft.RangeSlider(start_value=20, end_value=75, min=0, max=100, divisions=20), + ] + ) + + +def segmented_choice() -> ft.Control: + return ft.SegmentedButton( + segments=[ + ft.Segment(value="day", label="День", icon=ft.Icons.LIGHT_MODE), + ft.Segment(value="week", label="Неделя", icon=ft.Icons.DATE_RANGE), + ft.Segment(value="month", label="Месяц", icon=ft.Icons.CALENDAR_MONTH), + ], + selected=["week"], + ) + + +EXAMPLES = [ + Example("text-fields", "Text fields", "Ввод текста, пароля и многострочного значения.", "Ввод", text_fields), + Example("dropdowns", "Dropdown", "Выбор одного значения из меню.", "Ввод", dropdowns), + Example("selection-controls", "Checkbox, switch и radio", "Булевы и взаимоисключающие варианты.", "Ввод", selection_controls), + Example("sliders", "Sliders", "Одиночный и диапазонный выбор чисел.", "Ввод", sliders), + Example("segmented", "Segmented button", "Компактный выбор режима.", "Ввод", segmented_choice), +] diff --git a/src/gallery/examples/layout.py b/src/gallery/examples/layout.py new file mode 100644 index 0000000..9eb885d --- /dev/null +++ b/src/gallery/examples/layout.py @@ -0,0 +1,60 @@ +import flet as ft + +from gallery.models import Example + + +def row_and_column() -> ft.Control: + return ft.Column( + controls=[ + ft.Text("Row распределяет элементы по горизонтали"), + ft.Row( + controls=[ft.Container(bgcolor=c, width=64, height=64, border_radius=12) for c in (ft.Colors.PRIMARY, ft.Colors.SECONDARY, ft.Colors.TERTIARY)], + alignment=ft.MainAxisAlignment.SPACE_BETWEEN, + ), + ] + ) + + +def responsive_row() -> ft.Control: + return ft.ResponsiveRow( + controls=[ + ft.Container(content=ft.Text(f"col {i + 1}"), bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST, padding=20, border_radius=12, col={"xs": 12, "sm": 6, "md": 4}) + for i in range(3) + ], + spacing=12, + run_spacing=12, + ) + + +def stack() -> ft.Control: + return ft.Stack( + controls=[ + ft.Container(width=300, height=150, bgcolor=ft.Colors.PRIMARY_CONTAINER, border_radius=20), + ft.Container(content=ft.Text("Слой поверх фона", weight=ft.FontWeight.BOLD), left=24, bottom=24), + ft.Icon(ft.Icons.LAYERS, size=42, right=20, top=20), + ], + width=300, + height=150, + ) + + +def grid_view() -> ft.Control: + return ft.GridView( + controls=[ + ft.Container(content=ft.Text(str(i + 1)), alignment=ft.Alignment.CENTER, bgcolor=ft.Colors.SECONDARY_CONTAINER, border_radius=12) + for i in range(8) + ], + max_extent=100, + spacing=10, + run_spacing=10, + height=220, + ) + + +EXAMPLES = [ + Example("row-column", "Row и Column", "Базовая раскладка по двум осям.", "Макет", row_and_column), + Example("responsive-row", "ResponsiveRow", "Адаптивные колонки для разных экранов.", "Макет", responsive_row), + Example("stack", "Stack", "Наложение и точное позиционирование.", "Макет", stack), + Example("grid", "GridView", "Прокручиваемая сетка элементов.", "Макет", grid_view), +] + diff --git a/src/gallery/examples/navigation.py b/src/gallery/examples/navigation.py new file mode 100644 index 0000000..656ea12 --- /dev/null +++ b/src/gallery/examples/navigation.py @@ -0,0 +1,48 @@ +import flet as ft + +from gallery.models import Example + + +def tabs() -> ft.Control: + return ft.Tabs( + length=3, + selected_index=0, + content=ft.Column( + controls=[ + ft.TabBar(tabs=[ft.Tab(label="Обзор", icon=ft.Icons.DASHBOARD), ft.Tab(label="Код", icon=ft.Icons.CODE), ft.Tab(label="API", icon=ft.Icons.DATA_OBJECT)]), + ft.TabBarView(controls=[ft.Container(ft.Text("Обзор компонента"), padding=20), ft.Container(ft.Text("Пример кода"), padding=20), ft.Container(ft.Text("Справка API"), padding=20)]), + ] + ), + height=160, + ) + + +def navigation_bar() -> ft.Control: + return ft.NavigationBar( + destinations=[ + ft.NavigationBarDestination(icon=ft.Icons.HOME_OUTLINED, selected_icon=ft.Icons.HOME, label="Главная"), + ft.NavigationBarDestination(icon=ft.Icons.EXPLORE_OUTLINED, selected_icon=ft.Icons.EXPLORE, label="Обзор"), + ft.NavigationBarDestination(icon=ft.Icons.PERSON_OUTLINE, selected_icon=ft.Icons.PERSON, label="Профиль"), + ], + selected_index=1, + ) + + +def popup_menu() -> ft.Control: + return ft.PopupMenuButton( + icon=ft.Icons.MORE_VERT, + tooltip="Дополнительные действия", + items=[ + ft.PopupMenuItem("Редактировать", icon=ft.Icons.EDIT), + ft.PopupMenuItem("Дублировать", icon=ft.Icons.CONTENT_COPY), + ft.PopupMenuItem("Удалить", icon=ft.Icons.DELETE_OUTLINE), + ], + ) + + +EXAMPLES = [ + Example("tabs", "Tabs", "Переключение между равноправными разделами.", "Навигация", tabs), + Example("navigation-bar", "NavigationBar", "Основные разделы мобильного приложения.", "Навигация", navigation_bar), + Example("popup-menu", "PopupMenuButton", "Редкие действия в компактном меню.", "Навигация", popup_menu), +] + diff --git a/src/gallery/models.py b/src/gallery/models.py new file mode 100644 index 0000000..1f04827 --- /dev/null +++ b/src/gallery/models.py @@ -0,0 +1,21 @@ +from collections.abc import Callable +from dataclasses import dataclass + +import flet as ft + + +@dataclass(frozen=True) +class Example: + id: str + title: str + description: str + category: str + builder: Callable[[], ft.Control] + + +@dataclass +class GallerySettings: + theme_mode: ft.ThemeMode = ft.ThemeMode.SYSTEM + seed_color: str = "#6750A4" + show_source_buttons: bool = True + diff --git a/src/gallery/source_links.py b/src/gallery/source_links.py new file mode 100644 index 0000000..eb85097 --- /dev/null +++ b/src/gallery/source_links.py @@ -0,0 +1,20 @@ +import json +import os +from importlib.resources import files + +REPOSITORY = "https://git.snw.su/snw/FletAppDemo" + + +def _manifest() -> dict[str, dict[str, str | int]]: + resource = files("gallery").joinpath("source_manifest.json") + return json.loads(resource.read_text(encoding="utf-8")) + + +SOURCE_MANIFEST = _manifest() + + +def source_url(example_id: str) -> str: + anchor = SOURCE_MANIFEST[example_id] + revision = os.getenv("FLET_GALLERY_REVISION", "main") + return f"{REPOSITORY}/src/commit/{revision}/{anchor['path']}#L{anchor['line']}" + diff --git a/src/gallery/source_manifest.json b/src/gallery/source_manifest.json new file mode 100644 index 0000000..fe4ed6e --- /dev/null +++ b/src/gallery/source_manifest.json @@ -0,0 +1,137 @@ +{ + "bottom-sheet": { + "function": "bottom_sheet", + "line": 49, + "path": "src/gallery/examples/data_feedback.py" + }, + "cards": { + "function": "cards", + "line": 31, + "path": "src/gallery/examples/content.py" + }, + "chips": { + "function": "chips", + "line": 50, + "path": "src/gallery/examples/actions.py" + }, + "data-table": { + "function": "data_table", + "line": 6, + "path": "src/gallery/examples/data_feedback.py" + }, + "dialog": { + "function": "alert_dialog", + "line": 28, + "path": "src/gallery/examples/data_feedback.py" + }, + "dropdowns": { + "function": "dropdowns", + "line": 17, + "path": "src/gallery/examples/inputs.py" + }, + "fab": { + "function": "floating_action_button", + "line": 41, + "path": "src/gallery/examples/actions.py" + }, + "filled-buttons": { + "function": "filled_buttons", + "line": 6, + "path": "src/gallery/examples/actions.py" + }, + "grid": { + "function": "grid_view", + "line": 41, + "path": "src/gallery/examples/layout.py" + }, + "icon-buttons": { + "function": "icon_buttons", + "line": 31, + "path": "src/gallery/examples/actions.py" + }, + "icons-badges": { + "function": "icons_and_badges", + "line": 19, + "path": "src/gallery/examples/content.py" + }, + "lists": { + "function": "lists_and_expansion", + "line": 60, + "path": "src/gallery/examples/content.py" + }, + "markdown": { + "function": "markdown", + "line": 73, + "path": "src/gallery/examples/content.py" + }, + "navigation-bar": { + "function": "navigation_bar", + "line": 20, + "path": "src/gallery/examples/navigation.py" + }, + "outlined-buttons": { + "function": "outlined_and_text_buttons", + "line": 19, + "path": "src/gallery/examples/actions.py" + }, + "popup-menu": { + "function": "popup_menu", + "line": 31, + "path": "src/gallery/examples/navigation.py" + }, + "progress": { + "function": "progress_indicators", + "line": 17, + "path": "src/gallery/examples/data_feedback.py" + }, + "responsive-row": { + "function": "responsive_row", + "line": 18, + "path": "src/gallery/examples/layout.py" + }, + "row-column": { + "function": "row_and_column", + "line": 6, + "path": "src/gallery/examples/layout.py" + }, + "segmented": { + "function": "segmented_choice", + "line": 64, + "path": "src/gallery/examples/inputs.py" + }, + "selection-controls": { + "function": "selection_controls", + "line": 30, + "path": "src/gallery/examples/inputs.py" + }, + "sliders": { + "function": "sliders", + "line": 48, + "path": "src/gallery/examples/inputs.py" + }, + "snackbar": { + "function": "snackbar", + "line": 42, + "path": "src/gallery/examples/data_feedback.py" + }, + "stack": { + "function": "stack", + "line": 29, + "path": "src/gallery/examples/layout.py" + }, + "tabs": { + "function": "tabs", + "line": 6, + "path": "src/gallery/examples/navigation.py" + }, + "text-fields": { + "function": "text_fields", + "line": 6, + "path": "src/gallery/examples/inputs.py" + }, + "typography": { + "function": "typography", + "line": 6, + "path": "src/gallery/examples/content.py" + } +} diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..1f026f4 --- /dev/null +++ b/src/main.py @@ -0,0 +1,6 @@ +import flet as ft + +from gallery.app import main + +if __name__ == "__main__": + ft.run(main) diff --git a/tests/test_gallery.py b/tests/test_gallery.py new file mode 100644 index 0000000..31de425 --- /dev/null +++ b/tests/test_gallery.py @@ -0,0 +1,27 @@ +import json +from pathlib import Path + +from gallery.examples import ALL_EXAMPLES + +ROOT = Path(__file__).resolve().parents[1] + + +def test_example_ids_are_unique() -> None: + ids = [example.id for example in ALL_EXAMPLES] + assert len(ids) == len(set(ids)) + assert len(ids) >= 25 + + +def test_every_example_builds_a_control() -> None: + for example in ALL_EXAMPLES: + assert example.builder() is not None, example.id + + +def test_source_manifest_is_complete_and_exact() -> None: + manifest_path = ROOT / "src" / "gallery" / "source_manifest.json" + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + assert set(manifest) == {example.id for example in ALL_EXAMPLES} + for anchor in manifest.values(): + source = (ROOT / anchor["path"]).read_text(encoding="utf-8").splitlines() + assert source[anchor["line"] - 1].startswith(f"def {anchor['function']}(") + diff --git a/tools/generate_source_manifest.py b/tools/generate_source_manifest.py new file mode 100644 index 0000000..76acbcf --- /dev/null +++ b/tools/generate_source_manifest.py @@ -0,0 +1,52 @@ +"""Generate exact repository source anchors without importing application code.""" + +from __future__ import annotations + +import ast +import json +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +EXAMPLES_DIR = ROOT / "src" / "gallery" / "examples" +OUTPUT = ROOT / "src" / "gallery" / "source_manifest.json" + + +def collect() -> dict[str, dict[str, str | int]]: + manifest: dict[str, dict[str, str | int]] = {} + for path in sorted(EXAMPLES_DIR.glob("*.py")): + if path.name == "__init__.py": + continue + tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path)) + functions = { + node.name: node.lineno + for node in tree.body + if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) + } + for node in ast.walk(tree): + if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Name): + continue + if node.func.id != "Example" or len(node.args) < 5: + continue + example_id, builder = node.args[0], node.args[4] + if not isinstance(example_id, ast.Constant) or not isinstance(example_id.value, str): + raise TypeError(f"Example id must be a string in {path}") + if not isinstance(builder, ast.Name) or builder.id not in functions: + raise ValueError(f"Example {example_id.value} must reference a local function") + if example_id.value in manifest: + raise ValueError(f"Duplicate example id: {example_id.value}") + manifest[example_id.value] = { + "path": path.relative_to(ROOT).as_posix(), + "line": functions[builder.id], + "function": builder.id, + } + return manifest + + +def main() -> None: + content = json.dumps(collect(), ensure_ascii=False, indent=2, sort_keys=True) + "\n" + OUTPUT.write_text(content, encoding="utf-8") + print(f"Generated {OUTPUT.relative_to(ROOT)}") + + +if __name__ == "__main__": + main() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..cd3944d --- /dev/null +++ b/uv.lock @@ -0,0 +1,319 @@ +version = 1 +revision = 3 +requires-python = ">=3.11" + +[[package]] +name = "anyio" +version = "4.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, +] + +[[package]] +name = "certifi" +version = "2026.7.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "flet" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", marker = "sys_platform != 'emscripten'" }, + { name = "msgpack" }, + { name = "oauthlib", marker = "sys_platform != 'emscripten'" }, + { name = "repath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/0f/ac7e2af03fd49dd28477a062f8022a362155a2608857c79d6a4272e4f4a3/flet-1.0.1.tar.gz", hash = "sha256:0c11ea010efbae34600556d964613324ea0ddc711bdac94d6592699b28dc0ba4", size = 571070, upload-time = "2026-09-22T20:24:47.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/86/6eaa2f957931ee03f9814838259d5df395295c7807673b9ea2a30f75f4c5/flet-1.0.1-py3-none-any.whl", hash = "sha256:94a15d75f587dc0b6a2479348f2e4b2bf40606469eb5a18dc62ab12a790f1ae2", size = 633955, upload-time = "2026-09-22T20:24:46.225Z" }, +] + +[[package]] +name = "flet-component-gallery" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "flet" }, +] + +[package.dev-dependencies] +dev = [ + { name = "pytest" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [{ name = "flet", specifier = "==1.0.1" }] + +[package.metadata.requires-dev] +dev = [ + { name = "pytest", specifier = ">=8.4.0" }, + { name = "ruff", specifier = ">=0.13.0" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "idna" +version = "3.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/08/8eea9d4b8302028f3abb2c0813953f7aec26d33b7a8960ed760e65ff29fa/idna-3.20.tar.gz", hash = "sha256:a7db850025b95ded1eae8a46181a1a6c56c92c96f0e2b005d9ff8dc0210cab44", size = 216463, upload-time = "2026-09-17T14:11:04.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/a2/bb081bab032533a855d44de1d56f8e8426114ff1ba5d1f07a438a0a654f8/idna-3.20-py3-none-any.whl", hash = "sha256:ab7ae7122974553370f0bdb919e1a960b2cd1bc1ef0276416d896db81c14582c", size = 69583, upload-time = "2026-09-17T14:11:03.168Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "msgpack" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/ea2100ec54d30c46ee9dba10a3bfb79b655e96c6df237238a3234c75869b/msgpack-1.2.2.tar.gz", hash = "sha256:9eb0b0e602064527a045ea28c4f174ed69383587e29cebe28947e3b84106eb2a", size = 187025, upload-time = "2026-08-27T10:03:47.793Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/fd/a05ba8f84c5951c9aec2a19c1c81f6c4a67b8bec80af604ac5b23ccfa019/msgpack-1.2.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d7fb25b4442fae0cb2590272d06ab4f6caa526ee36a994edb81e946b874813e", size = 83498, upload-time = "2026-08-27T10:01:50.62Z" }, + { url = "https://files.pythonhosted.org/packages/0f/df/e20bcf5c149890545334743b212eb4b82e1a25fe0a34f99753a1755bfab5/msgpack-1.2.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7fe374ba76eb0ecca13a1703daa8fa85825a6ddddbb52d4c1a732fa524194683", size = 83896, upload-time = "2026-08-27T10:01:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c3/00dcd902d66a641b9ba350783feb482ea5c1ca4a7ff6629db0c10c0ea982/msgpack-1.2.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9b0c1f2aa7b0026b4bd50718100e8b04175e4f36e160aa852502377b5e572e7", size = 413259, upload-time = "2026-08-27T10:01:53.296Z" }, + { url = "https://files.pythonhosted.org/packages/93/15/17374efe9793f5332c7d4727ab40539f95a1dc9df653531795daca8c4281/msgpack-1.2.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f11e09f10210a91c169e39c7a5a1f9090eaa73ad75555fafad5023c3053c47ba", size = 422907, upload-time = "2026-08-27T10:01:54.786Z" }, + { url = "https://files.pythonhosted.org/packages/d3/af/2b567d684f912fedcefe3f7c37de604716ffa99336bd432688f9f040df92/msgpack-1.2.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8b1415d02e9bf722672af8a90f90813265a0cd0b14163187261e54a5592bc949", size = 389248, upload-time = "2026-08-27T10:01:56.55Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/d8533fed473cc3e309a701e851d0e5fe36ada5552a3899025f5c69fbe877/msgpack-1.2.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:42fd9260416885b4815caca5bdd14dfd5dda6cdade732d6c09104ef8f6228761", size = 407099, upload-time = "2026-08-27T10:01:58.357Z" }, + { url = "https://files.pythonhosted.org/packages/d6/1b/57906337bfee0ead554571dc203ea17c3fad26d51e5eca6271ecd983f73b/msgpack-1.2.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:336525cc2688e43ea77dfb1a4ce012c8cde561835913801dbfcfdcf4111d8abb", size = 387201, upload-time = "2026-08-27T10:02:00.109Z" }, + { url = "https://files.pythonhosted.org/packages/de/0f/5d1e6d68e516621697a9262b24917d678793e838cf3f331ed4656b3e959d/msgpack-1.2.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cdb6cc6e1127d15879c47a8b3270716243da82d3e7feab1f5946872c75b3d60f", size = 420765, upload-time = "2026-08-27T10:02:01.573Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a6/07f9a4f3324d55c3567ab2a7e8d5325291bc95a31a374bb390a21b7c4e24/msgpack-1.2.2-cp311-cp311-win32.whl", hash = "sha256:cf66fb38703e61a486b01b56d43bb1f50698fbe99b6bd90feba10f24fab60b3b", size = 64785, upload-time = "2026-08-27T10:02:03.01Z" }, + { url = "https://files.pythonhosted.org/packages/84/13/f748f0d59f355d196e71a0b32d48d386a9bd311f94d954e666cf7e5b2572/msgpack-1.2.2-cp311-cp311-win_amd64.whl", hash = "sha256:0883a1578168929fd1640fbbc4614773f1a130e419a8a817dc2918d9af1b651c", size = 71258, upload-time = "2026-08-27T10:02:04.375Z" }, + { url = "https://files.pythonhosted.org/packages/46/a6/10d979c4e76b18a9b9ebbd6499ff863474ffe5955028ea27e09b66f6833c/msgpack-1.2.2-cp311-cp311-win_arm64.whl", hash = "sha256:4955accbd87f27beebef5f3ecc27503aa74cb016fb4f640868e749fd93194a35", size = 65860, upload-time = "2026-08-27T10:02:05.735Z" }, + { url = "https://files.pythonhosted.org/packages/31/78/90c15bebb1a72667349ca62d4507e9d9369e7f8f76b95f490b823d3622e5/msgpack-1.2.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a4348705be86e029d04e741cf9ed0dfe03e942d7d3b92e838fa80d3aa2c3ebc", size = 84275, upload-time = "2026-08-27T10:02:07.106Z" }, + { url = "https://files.pythonhosted.org/packages/88/88/c2b6d8e81571da87aa232c0e34a3f3a0e618e6235892065ec82d1d81fc7a/msgpack-1.2.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a652ceeededf71d3fa40c303a02a149d42338d310162367b91c539d4bd6e0a3", size = 83970, upload-time = "2026-08-27T10:02:08.488Z" }, + { url = "https://files.pythonhosted.org/packages/da/c0/d3ede9f5d16acb4c05a9281859f1e99ef9f877a928eb78454c37f70db001/msgpack-1.2.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90986cc9aab9d7d1d8f38bcbf65d3f7ac83bdd90c35765db7d691b4829698cba", size = 409401, upload-time = "2026-08-27T10:02:09.877Z" }, + { url = "https://files.pythonhosted.org/packages/41/f0/29f591bea185616cf417645ac03bd3ad9b317483ad8572160e325f7fe777/msgpack-1.2.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:77c2e018417dc1d66f235e383877ee885b60ade9d29e494dd581e08af2cb1923", size = 420619, upload-time = "2026-08-27T10:02:11.526Z" }, + { url = "https://files.pythonhosted.org/packages/4e/8e/c70c8c9180c5ddf4440eb8658ebead98e22e7686fbf84f6b165031430750/msgpack-1.2.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0e91332144f69bc3018c91232fac26da580ef748fb8eaddd7914d4458001cc4f", size = 379747, upload-time = "2026-08-27T10:02:13.345Z" }, + { url = "https://files.pythonhosted.org/packages/50/9a/f10ce11fa62700c9ab87a22e65b9ca272f7f673ddd31aeb2de6ae272ad35/msgpack-1.2.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e915d390d7068b257ca8b62f3fc59fad135c8631d1017ab03b0b924b07c5367", size = 398944, upload-time = "2026-08-27T10:02:15.006Z" }, + { url = "https://files.pythonhosted.org/packages/82/fe/d7be978456ff8552e69a8e270d882e7530e01513c096b293d83df03753ea/msgpack-1.2.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c522420d78db2431887d45b518e304d86e27b9ad0b30f24e3806a6ad5d8bdbfc", size = 373979, upload-time = "2026-08-27T10:02:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/be/af/91b0d8d3fb3063e259daee3ea8515cea6282f68f4b0e5f0b6fea25762c6e/msgpack-1.2.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4b554d8164ebb526892194f71dcd96ef1fefe0c250087498785d3ffc04a80be3", size = 417781, upload-time = "2026-08-27T10:02:18.293Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3c/ce8e9efe1fd9e95c78b3705e4300ba7feba3dc6c00fb76259895db155518/msgpack-1.2.2-cp312-cp312-win32.whl", hash = "sha256:0e3315de5a4b2920ccef48d96b4448025e064a10d0f5a250f6584477d839c8d4", size = 65267, upload-time = "2026-08-27T10:02:19.869Z" }, + { url = "https://files.pythonhosted.org/packages/85/98/a33b8b4af14e3476bb0da1b8c36ef7a0f28dcf95db1c5e68ff88cb89d591/msgpack-1.2.2-cp312-cp312-win_amd64.whl", hash = "sha256:b68614fba0570349833b7dd999ff0aed4e5cc8d9eb6e3a7d4527be33c65e33d3", size = 72275, upload-time = "2026-08-27T10:02:21.141Z" }, + { url = "https://files.pythonhosted.org/packages/df/5e/2f323a33a6aba5bd4b2d8b430e4fab21d92cd91c093b49ee287bc166ee54/msgpack-1.2.2-cp312-cp312-win_arm64.whl", hash = "sha256:59d5b93efa45fd09f620d0c9ba81cde339a2c9937af3eea42ee9653094ce6640", size = 65488, upload-time = "2026-08-27T10:02:22.575Z" }, + { url = "https://files.pythonhosted.org/packages/1f/eb/42f31c5a48811787ff59a9869721f70a49654d65ab6c455f4463c39b044e/msgpack-1.2.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8b2a281b556f120a43e591ea39915741b7ad54d4727b9c4350a0a11692252533", size = 83911, upload-time = "2026-08-27T10:02:24.06Z" }, + { url = "https://files.pythonhosted.org/packages/33/54/10c6c16ddba8a5112e3680176b838e3694e4aad7284f9daa6d6d70d98817/msgpack-1.2.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e8cdd1f3e7cc52c751092a9bf740e81e6919ab109cd376ae2d965dad0bbae34", size = 83734, upload-time = "2026-08-27T10:02:25.613Z" }, + { url = "https://files.pythonhosted.org/packages/d7/75/35823e4419df8792191b2a17ae3fe71b41d02c162b2c491c94d1a87f0caa/msgpack-1.2.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1814f92306ae7862908e9ece7cfd90e0dc87ded3e89b6ae7ffdd1175d6376fdc", size = 405635, upload-time = "2026-08-27T10:02:27.012Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d3/6592e4064619b04f2dd0054c5fa13e37e3d55eb26044483d871fadb2f46b/msgpack-1.2.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d24b38a825bcca41bb956de50eb98451ef291304a8607fad99e619043d3e79b9", size = 417332, upload-time = "2026-08-27T10:02:28.776Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a1/b21c6818a545e9a4a976ac954a5c250eecde9a02e0ec82f415473dab1324/msgpack-1.2.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34e83e345194a2a51d8bd447dea9de2104f91e75b247f4735f14f04529f0746b", size = 374378, upload-time = "2026-08-27T10:02:30.678Z" }, + { url = "https://files.pythonhosted.org/packages/03/8b/7ada15c7b64151d6dbb562d1b091520efb2c37acf2403b1d4ae13797b27d/msgpack-1.2.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:682804bf31e43d46e51a9a33bd575b51e839d715ce6bd5612c055f7b28ad637b", size = 395809, upload-time = "2026-08-27T10:02:32.322Z" }, + { url = "https://files.pythonhosted.org/packages/bb/f7/96283e50f7020df4dfeacc55612b7a210c8cdf0dda48bc262f1f9b3e4c49/msgpack-1.2.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:9b659d77f8726fa5e7038967dda6b68d53cf34472c094cfa5b845454713b90d5", size = 373495, upload-time = "2026-08-27T10:02:33.832Z" }, + { url = "https://files.pythonhosted.org/packages/cc/fe/1548dede9d9ca482f2d424a2e110a9705d4e02627a16b8bc8d10ce0208a2/msgpack-1.2.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d9a562aec0a92fe536da2e533d313b3d2a6b929157b1dec7ff623446dc0a8ab", size = 414360, upload-time = "2026-08-27T10:02:35.396Z" }, + { url = "https://files.pythonhosted.org/packages/77/9d/4419b8f86c219174b1fb8bbd7faaf84a548935f7b1916d028401b9433417/msgpack-1.2.2-cp313-cp313-win32.whl", hash = "sha256:a4161eee7799863aee237c35c90427861f7b994416dd81ae829f560b0a81bdcd", size = 65196, upload-time = "2026-08-27T10:02:37.007Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f8/593f5caf0dacab41cde1564c5f0419e61af55ec9628006205e8fd5eb5e03/msgpack-1.2.2-cp313-cp313-win_amd64.whl", hash = "sha256:b07c03f0da7e5279170df7745ddc732d526c8a198208936ec1a95c11ed2b2d5f", size = 72203, upload-time = "2026-08-27T10:02:38.28Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/c6ef92046b4a2bbb9d3aa0cb581cbf4a4051afccf6e5fb301a1bd3086f39/msgpack-1.2.2-cp313-cp313-win_arm64.whl", hash = "sha256:d13d07efbf655f9ae7a2352b630c52727b359005b21ba08a507585c9ac8c0896", size = 65435, upload-time = "2026-08-27T10:02:39.534Z" }, + { url = "https://files.pythonhosted.org/packages/5e/50/3e92c403346652cabd08cb8faceef847bae917ea3b3c81b64a5b6d09ed41/msgpack-1.2.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e497ee34e8a3342bbde51b27c22d8db05a651df3361dd3daef5b3ab0d66f3e04", size = 84315, upload-time = "2026-08-27T10:02:41.181Z" }, + { url = "https://files.pythonhosted.org/packages/b6/dc/8efe6dd96a12ab043930cb4cffb40b6e7f061491d6ec7a3d2b75ef1fda42/msgpack-1.2.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0dd9173c5ebaf5ecc5ca86e7ae1db92934e1d57b856f3dd90698941431f4fd77", size = 84634, upload-time = "2026-08-27T10:02:42.621Z" }, + { url = "https://files.pythonhosted.org/packages/1b/89/996573095bf7b038c04dd65ddbc4f1a4d381b0f7a44ff9186f3c7b8325c2/msgpack-1.2.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8dc4487097571f7311188c3eca2a3e86cd1f1db4c37c7a017bcc3fd38486cbfe", size = 404194, upload-time = "2026-08-27T10:02:44.096Z" }, + { url = "https://files.pythonhosted.org/packages/b6/4e/46f5a5d949dbd054dab60cb15aac7ac6ae6774c134532893414689bf2f53/msgpack-1.2.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:73b0e05c32c3cfc3cd84994908e57430c0ebc6813abf905d3f18ff115d54df3f", size = 412343, upload-time = "2026-08-27T10:02:45.747Z" }, + { url = "https://files.pythonhosted.org/packages/da/e8/739a94197358a313307e6e9e7d8d22ef66add39222de911a44161aa96920/msgpack-1.2.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa1120c653b76d8eafa50423b5eba06b5c9737f8692c74fa3afe03e84b8978ea", size = 372620, upload-time = "2026-08-27T10:02:47.578Z" }, + { url = "https://files.pythonhosted.org/packages/03/d4/09b92e1fcdccea9466bfae45455367ac52362ae445d96a602e51b7a8df73/msgpack-1.2.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccfd880988f8438d1c91c77d7edc58e70f4d2012e999167bc154c64c6f06ea6b", size = 394603, upload-time = "2026-08-27T10:02:49.172Z" }, + { url = "https://files.pythonhosted.org/packages/47/db/d11bd6f258a60703dcdc7a3772818ad0c2f602ee4c2acfb24088c6c3ebc3/msgpack-1.2.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:6195257a107bf25872ef84aab7295078271eea3ac6413f0506b631f6c9586ed5", size = 372666, upload-time = "2026-08-27T10:02:50.886Z" }, + { url = "https://files.pythonhosted.org/packages/71/cf/fbbbac0c6e5fbb9d51abc23e3b5fe8620f5c01e0588797cf664a623bb9e1/msgpack-1.2.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b8dd6c71d20c28d2d0eb0c51e7cccf3584afde3b1364f6629596186c9025bd54", size = 410889, upload-time = "2026-08-27T10:02:52.51Z" }, + { url = "https://files.pythonhosted.org/packages/94/60/8366558da954095e04e7fbc351f9387d87a682feaee9a235ceda966f794b/msgpack-1.2.2-cp314-cp314-win32.whl", hash = "sha256:d242f3c4ccf55b056e6cf901720dccde58f1df117898f2bbf3bcd6e38ec7c248", size = 66774, upload-time = "2026-08-27T10:02:53.984Z" }, + { url = "https://files.pythonhosted.org/packages/4b/3d/1ce873c8057c65e4fbb076ffe1c99c9ae39d90a00a2540d7b06c652a292f/msgpack-1.2.2-cp314-cp314-win_amd64.whl", hash = "sha256:1510f24612d4b983dff6935d9273e02c320cfd525727fbcb58836a75f589fdbc", size = 73424, upload-time = "2026-08-27T10:02:55.277Z" }, + { url = "https://files.pythonhosted.org/packages/d5/55/e36f2a33e38657f33850d74e0bf256838a0d45802c298cc501a32bffcc08/msgpack-1.2.2-cp314-cp314-win_arm64.whl", hash = "sha256:7826f16edc763e768404f55605ef85dfcf5857e729c1ed29e0d7c180be4fe6d8", size = 67657, upload-time = "2026-08-27T10:02:56.493Z" }, + { url = "https://files.pythonhosted.org/packages/64/58/7e764b957bae80ae281a9cb28761068c8bae8d5c6ac0873e43cc69d176c7/msgpack-1.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f466049b8e1ec0854287bbe9a074316826fe0e08dcf707245f98b1ae49e92650", size = 86594, upload-time = "2026-08-27T10:02:57.796Z" }, + { url = "https://files.pythonhosted.org/packages/ae/f0/250f5985b6ee533e60d357571a808aaae03c54118294dc3db7158e27feb1/msgpack-1.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f6b6f8deb07d49090e1808c6ef9cb7d23ca17bef3aa6ed3e5e03df16606e60c", size = 87374, upload-time = "2026-08-27T10:02:59.256Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2c/126ec8f187877c5f688631c543d1d3a3d75b2e66b83fb9de3ed7c13a39b6/msgpack-1.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b542ffc0a5c531eedc40419f291f1bd659aa8d4223408a5b51c88a2796083fd3", size = 428157, upload-time = "2026-08-27T10:03:00.9Z" }, + { url = "https://files.pythonhosted.org/packages/95/21/d2d81d50aaedb14147d01f22094185794db3ad8a8791b60afacba0627c89/msgpack-1.2.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d095df2627e5dd59ac7b0c5ad627a671c76e6020171e03cbe4621a61f0562c3", size = 426669, upload-time = "2026-08-27T10:03:02.457Z" }, + { url = "https://files.pythonhosted.org/packages/c6/fc/f7d484ee5b572719608e7ffad569bea22ff11309a96ca2fae85eec94226b/msgpack-1.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ffdd2f4950daf7815490f23087963e3420175b9609520b7ff5df64d351159c22", size = 380625, upload-time = "2026-08-27T10:03:04.244Z" }, + { url = "https://files.pythonhosted.org/packages/0a/c4/b924cbd5516676f4e612329f18602a833bd055ffbe27f808eeba0f01bfea/msgpack-1.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:652d1bf13d01bac8fd569def0fe76745e55bcda01e30aa6332d5947ea3788839", size = 411328, upload-time = "2026-08-27T10:03:05.869Z" }, + { url = "https://files.pythonhosted.org/packages/27/9d/0c1d9683a951a80f270c3b7dac1022c18b9307617344dd44d904135d5e12/msgpack-1.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9bf452ff4d4981f25a18e9476e002bcc9263e7928024aa4d7148e25f7be3f929", size = 377892, upload-time = "2026-08-27T10:03:07.37Z" }, + { url = "https://files.pythonhosted.org/packages/06/bb/bf22338cdd22e0b40c8f28468cea5f3d9c320244c095d8303364bc012c41/msgpack-1.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:55faa6f8395e23b848c535ad5dcb96b3462f37f5e7f4ac500d500434f7345da7", size = 419426, upload-time = "2026-08-27T10:03:09Z" }, + { url = "https://files.pythonhosted.org/packages/7d/42/6d02c19a01abd8d7ce817c321d2ee6af1a8e24d584dca619d1b6576a83bf/msgpack-1.2.2-cp314-cp314t-win32.whl", hash = "sha256:419a45c67a5c04213172a14b1864657e014665b77d7081b107a51707923dd39e", size = 71810, upload-time = "2026-08-27T10:03:10.498Z" }, + { url = "https://files.pythonhosted.org/packages/af/df/fda3a204415dab0a8c0db5461ef7205416ea52bd8581c5cafd361be07f3b/msgpack-1.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:935b1cfad9b908b0fa845010f4271df4c2f04e1cd26e3f18acd61a45f93c9e36", size = 78919, upload-time = "2026-08-27T10:03:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/63/d4/4b4b0ef25a86deca91feaf7252ca885ba4f2ada40461379120122a04fe96/msgpack-1.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11e8c421e117d1c36728b423d0402555cccbf0c6f53e288f0e75b6b12100d70f", size = 71925, upload-time = "2026-08-27T10:03:13.332Z" }, + { url = "https://files.pythonhosted.org/packages/3b/92/4b44bc8f3243ef8cf9cb5368c17a299d45b9df858f6dfdd98a0482dbbb37/msgpack-1.2.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:e1b99ad34613d5f8477fa5cf99bc4eaeaf27965588007c102370cd9a78fe9de5", size = 84293, upload-time = "2026-08-27T10:03:14.718Z" }, + { url = "https://files.pythonhosted.org/packages/80/05/c992bb65744665a41b5bf531fc0e1619bae0901f57738228ded90023c151/msgpack-1.2.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:0fbc1bed8a535389b41882cfae66376e248cd1680eaa94fd83193c73e1d24986", size = 84490, upload-time = "2026-08-27T10:03:16.12Z" }, + { url = "https://files.pythonhosted.org/packages/d7/bf/7f53b9e6709a4df7f9b9b81dc65f9dfaa32caf65bee94986ec2cb8fa07f1/msgpack-1.2.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:06d95f61de7afe4f4ff908a6feebfcb070d0582ac87c9cf3cedf8551cf634516", size = 405332, upload-time = "2026-08-27T10:03:17.692Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5a/305c4dca14b50d0b51fb88ef04ec125b8f0be3e2ce730dcc62dbaa651cc5/msgpack-1.2.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b5c696ae7cd7166b3657261adb855b461ff31f07823fdbae9de8bf80adfccc21", size = 416798, upload-time = "2026-08-27T10:03:19.389Z" }, + { url = "https://files.pythonhosted.org/packages/7a/df/a645102b4cdfd9a94201cac4e900e9c1429fc16d86aa311c06eef82528c9/msgpack-1.2.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0708afbf6a9587f0bfe479a9825c141d14d91e2f6a5c8103cf28bc96f4edb5d9", size = 377312, upload-time = "2026-08-27T10:03:20.928Z" }, + { url = "https://files.pythonhosted.org/packages/f4/26/c56d8d086d3fb1077bb48092b158b5ea2eee08b279e10c191275f13bc980/msgpack-1.2.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:226a62ffe99fe54c5c61d910ec64c3449b7766c3280bd286bf6c94838dde239a", size = 395182, upload-time = "2026-08-27T10:03:22.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b5/3d46ba367a565e536d8d2a61eebcee71b1dc803da3ce74a22313b573d6fa/msgpack-1.2.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:9fd7f32e2f0fb334e7ecc5adb5cf0458785bd3a9d9d86f950e1715f101cebce5", size = 377945, upload-time = "2026-08-27T10:03:24.151Z" }, + { url = "https://files.pythonhosted.org/packages/a5/2c/d5d2df273ed5306357da25b69400fd8d7a53c4d87d8976604b677484d61c/msgpack-1.2.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:9db1ba1c1e6a84245a9dd866265b56b8a1e9461549cc72ed296d8cbfbd32961b", size = 413341, upload-time = "2026-08-27T10:03:25.85Z" }, + { url = "https://files.pythonhosted.org/packages/1b/fb/32613bced3cad47b40b1b73dd04d687121349d83f748efc2575929121903/msgpack-1.2.2-cp315-cp315-win32.whl", hash = "sha256:e2eb7ea0ac3911a7aac9d8aaa36d40f216d99455b3274cd3fac38181bcd910cf", size = 66730, upload-time = "2026-08-27T10:03:27.294Z" }, + { url = "https://files.pythonhosted.org/packages/74/56/d86171f7251015e9312e5a7f9fdd4cf89752fc2114b88fed453d2a040c66/msgpack-1.2.2-cp315-cp315-win_amd64.whl", hash = "sha256:9352e6cdb510a7b1a5d3ccaccec730e82e50cf3484a3af7bdaab19e23b9589ff", size = 73477, upload-time = "2026-08-27T10:03:28.615Z" }, + { url = "https://files.pythonhosted.org/packages/13/1a/56b90f6defef61700b86baca3637c15f62ac0f9b21ab0f16613ab9d1f101/msgpack-1.2.2-cp315-cp315-win_arm64.whl", hash = "sha256:29cc2d5291711a52956a79a51f41c732329df39ad727c886bd8f0b5b9237a808", size = 67660, upload-time = "2026-08-27T10:03:29.895Z" }, + { url = "https://files.pythonhosted.org/packages/cd/20/12751ca0d8ec874701b54c392c2b19f51af8dd1de40a92a10e356f0aaf58/msgpack-1.2.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:d886baa46b2532135e7320067e6a44edb09ba5883a6096b0f9c044533984b8a8", size = 86462, upload-time = "2026-08-27T10:03:31.348Z" }, + { url = "https://files.pythonhosted.org/packages/91/4c/cf6d12a3d709fe5f9771dd917c35e6ebcd55597a5b792287382fde056c95/msgpack-1.2.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53679573c75cce5f82359e0bd4e6a97809a6b9a9b7a48fd1ba592f4a82cddc84", size = 87412, upload-time = "2026-08-27T10:03:32.74Z" }, + { url = "https://files.pythonhosted.org/packages/ca/0d/0aac5752d1708dcb458f8754db34a4999514db3df2d2b798b9381293f638/msgpack-1.2.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d3c247d457ae9079974c7ce3c665396754a6d2baff7eaa51332212a8a5a3f13b", size = 422057, upload-time = "2026-08-27T10:03:34.124Z" }, + { url = "https://files.pythonhosted.org/packages/81/30/70f281a3685b04aaf235a5237da11b978a02a865a5a479186205177ad676/msgpack-1.2.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:352ed831042549cca8be23780e1fe7c9177e65ff02bf183509c4b4d33f671782", size = 422696, upload-time = "2026-08-27T10:03:35.862Z" }, + { url = "https://files.pythonhosted.org/packages/8a/6d/f76e8425efb0aa38988cd778ae290bfa120491d80d26872d88bb52fedb3f/msgpack-1.2.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f80361592c13d7226b4379c8941529b63fe1a9d0e05d2de8f3306b70e522b53f", size = 376495, upload-time = "2026-08-27T10:03:37.339Z" }, + { url = "https://files.pythonhosted.org/packages/95/77/0809aa9b52b2868f7d01862dc14073708f0440421a65197b48453480034c/msgpack-1.2.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:68df2947921d449f6dcfeafd86cb2cdde13327a8b447534bbe4ee5aaf32a5695", size = 404683, upload-time = "2026-08-27T10:03:38.87Z" }, + { url = "https://files.pythonhosted.org/packages/02/d2/4e5ac915ba120172d210ef00165c5e6276c8a65db3a4a5cf36e946b83e23/msgpack-1.2.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:51dd39d23cfdea0400ed3ff2d29d1e83bd951d3aea79dc89be5b701a09edfe23", size = 375087, upload-time = "2026-08-27T10:03:40.486Z" }, + { url = "https://files.pythonhosted.org/packages/cb/e3/8051d53e5495c87c6cf27eb42fb680361017037f87f322bdaf525f71e4a2/msgpack-1.2.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b13b59e66f107cca1ba708dd5307179870ca1b15b19fcee7ccf722e5308d9212", size = 414421, upload-time = "2026-08-27T10:03:42.308Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4e/13783aa7c17414d7186c72c49bc718366f75e49f0ea58d4f81cb63ac3187/msgpack-1.2.2-cp315-cp315t-win32.whl", hash = "sha256:8c6321a414f8b4a8dc43976b2fa8349156434ca9adedd9a187b796f7e1d3d3fc", size = 71790, upload-time = "2026-08-27T10:03:43.715Z" }, + { url = "https://files.pythonhosted.org/packages/5e/9d/1d02994c7ae2603c98100984428ff0f67443572133bc18eca6058f732c1b/msgpack-1.2.2-cp315-cp315t-win_amd64.whl", hash = "sha256:6f53285f20d592ed309ee19e509cc4c77a3bda1db02ad67e8a0949bb227a5a6d", size = 78766, upload-time = "2026-08-27T10:03:45.036Z" }, + { url = "https://files.pythonhosted.org/packages/60/54/89ed16e6f966a050dc78b0e94a545025211b07ce9f4bdfe07dff70c03fc2/msgpack-1.2.2-cp315-cp315t-win_arm64.whl", hash = "sha256:a378e12ccc06d76efde115caf4073b7e5ff3cc18291d1341f9e65fb882e3f754", size = 71819, upload-time = "2026-08-27T10:03:46.375Z" }, +] + +[[package]] +name = "oauthlib" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918, upload-time = "2025-06-19T22:48:08.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065, upload-time = "2025-06-19T22:48:06.508Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "repath" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/65/e1/824989291d0f01886074fdf9504ba54598f5665bc4dd373b589b87e76608/repath-0.9.0.tar.gz", hash = "sha256:8292139bac6a0e43fd9d70605d4e8daeb25d46672e484ed31a24c7ce0aef0fb7", size = 5492, upload-time = "2019-10-08T00:25:22.3Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/ed/92e9b8a3ffc562f21df14ef2538f54e911df29730e1f0d79130a4edc86e7/repath-0.9.0-py3-none-any.whl", hash = "sha256:ee079d6c91faeb843274d22d8f786094ee01316ecfe293a1eb6546312bb6a318", size = 4738, upload-time = "2019-10-08T00:25:20.842Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/78/449cb84790bd5cc3823b2652ee405a4558856e5c4195aee3a16bf7b3eb5d/ruff-0.16.8.tar.gz", hash = "sha256:9247bf92b5f04d825c8639a4fe423ec2e4222acd9222e58412b0dab7e442798b", size = 4938814, upload-time = "2026-09-16T15:54:46.688Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/25/6071aabc530e9be7e2c195e8fe3f7aea2735405b6cf447212832d7811831/ruff-0.16.8-py3-none-linux_armv6l.whl", hash = "sha256:6ffbd6d87383c1edf5f6fa890f10200950240d7c1a16052a19a09d3a2307dd38", size = 10048966, upload-time = "2026-09-16T15:53:57.605Z" }, + { url = "https://files.pythonhosted.org/packages/54/98/07f90ecbc74dd5fb5764f11f2bc774d6a7cffef92d2ff5f5b4e9e23c754e/ruff-0.16.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:42ed6b878ed61e3acca92f2730a17acff39286944ea82398544696366a6f925e", size = 10165498, upload-time = "2026-09-16T15:54:01.14Z" }, + { url = "https://files.pythonhosted.org/packages/fe/1f/e6a712e3b47cad4a40600134105ed193cb773f618a42eb7ba323cb812cc0/ruff-0.16.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:7ea781c7f2afba8c6a505ea0fb3f994020249e0c450635f5381286fea6b46170", size = 9830004, upload-time = "2026-09-16T15:54:03.998Z" }, + { url = "https://files.pythonhosted.org/packages/23/f2/311a08776d75d81c7676e20b6b020ae63cbe881fcdc7a8dd64e6e18bdd93/ruff-0.16.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8efeae3bbe414a5efefda11a792dfb51ef90ac48d50c4830de2f644caf3e8659", size = 9986558, upload-time = "2026-09-16T15:54:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ed/37b6cb3d3ba8c73e68ae3eb1d502383beb5aa05a582bb7bb3a922f929f54/ruff-0.16.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a79b795469fef7fc6e908b218eed2eb17332afd85031db6480dc864560e69b2", size = 9877332, upload-time = "2026-09-16T15:54:09.552Z" }, + { url = "https://files.pythonhosted.org/packages/22/cc/40873a8f36ad084cc540d55fcca7077264d5b13b24659e9180c176fb2b08/ruff-0.16.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fdc5563cdc50555e6fba39322850860e9267c1b3d12c26a74729d8604c3c812", size = 10507125, upload-time = "2026-09-16T15:54:12.152Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e4/fc91a642b78ccbab6b9477720f3644ae7a10a9bcce69a934679cd64f62bc/ruff-0.16.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:34508983c70665578dab88f5223d8e6228307e1135398ca8bfc8b7e9501e282b", size = 11336694, upload-time = "2026-09-16T15:54:15.489Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/bbd2a9a600a4e73dc3e7548a249c8d1671273464b55822c6fae50f602dff/ruff-0.16.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:644bb578569e0ffc575741232bd385dacdd6fbe123f1a729e7a225f54aa3957f", size = 10774448, upload-time = "2026-09-16T15:54:18.16Z" }, + { url = "https://files.pythonhosted.org/packages/1a/41/d83af9879a7b6e8bf5fe16b1da0b134049d2f5d3afac12defb0897cb84bd/ruff-0.16.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15e7d226246961db9235098333caa13063906d3851136b84c2900b82f5daa1df", size = 10323796, upload-time = "2026-09-16T15:54:20.743Z" }, + { url = "https://files.pythonhosted.org/packages/f5/2c/cefd07bfe914b84943ea769ade8d607bd22750b965d3228eefd7cebd15d0/ruff-0.16.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a2bf6bc3e9ebdd4449abc6f06cf64b98051a2c61cf94d2fe9596518c881f1a1e", size = 10514115, upload-time = "2026-09-16T15:54:23.497Z" }, + { url = "https://files.pythonhosted.org/packages/f3/9d/76a2e26c79a23be6e6e3664c57bec9e9fc8de155cfb9e4b67ea91b64f9d7/ruff-0.16.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ca111ba0849539165e9e59d2b442542f3c1e8060ebbdea82494f1ffbccb1e1f", size = 10072582, upload-time = "2026-09-16T15:54:26.185Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d4/f42edddb39668af1a559ceafa3823aedd65633a48dc9768e775485faa2c1/ruff-0.16.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:359a1e5b495448ee1e91018064382ebc86f90e8aac2fed222c7d0e4e8df85fd2", size = 9879644, upload-time = "2026-09-16T15:54:29.278Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/913e3195d95e0378786c6656945c865f534a3560e29139da4882aff630d1/ruff-0.16.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:59e8f5681349474110b24d62e93cfda6593f5fa3473446ca3705200cac1a08b9", size = 10231569, upload-time = "2026-09-16T15:54:32.036Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c4/8aa6ea0bdcedbd1bf87397e2fc4ed8406448ea5842f8660bc6e5f163039d/ruff-0.16.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:efa3e7a16d1baaa79957888dfdf8be9ef2e44db81cb032af06d76632ab59e773", size = 10663666, upload-time = "2026-09-16T15:54:34.838Z" }, + { url = "https://files.pythonhosted.org/packages/3d/02/7f10ef4700bc223c30a3fdd10631a29830c45524b810a3c7ed947af64591/ruff-0.16.8-py3-none-win32.whl", hash = "sha256:55793ba85c69921e89be061426d91a78652d6e50317c962240922747a4eb713f", size = 10093472, upload-time = "2026-09-16T15:54:37.47Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5d/a509c07d714b6da88f2c518b4637cf6f1d46b074be8f0f1e5fb9ff5126fe/ruff-0.16.8-py3-none-win_amd64.whl", hash = "sha256:a6b85621fd3c81e31fc5f5add09c9c078b430db3595ca632efafdec9e64ebfaa", size = 10586899, upload-time = "2026-09-16T15:54:40.488Z" }, + { url = "https://files.pythonhosted.org/packages/fe/a0/50787329e4f20bf9dc9f6230015d46ec69c51a97ace5bc202dae4755365d/ruff-0.16.8-py3-none-win_arm64.whl", hash = "sha256:d075e820af612102ce217f07cc93e69f9490b10ec13ea85fa87bd03d996cef8a", size = 10386316, upload-time = "2026-09-16T15:54:43.332Z" }, +] + +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +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" }, +]