From 74b41dea96b56c3f327a0f8db5c2d673d140129f Mon Sep 17 00:00:00 2001 From: Daniel Girtler Date: Mon, 24 Feb 2025 17:57:26 +1100 Subject: [PATCH] Add additional package selector (#3196) --- archinstall/__init__.py | 18 +- archinstall/lib/global_menu.py | 5 +- archinstall/lib/interactions/general_conf.py | 73 +- archinstall/lib/models/gen.py | 72 +- archinstall/lib/packages/__init__.py | 2 +- archinstall/lib/packages/packages.py | 62 +- archinstall/tui/curses_menu.py | 385 +++------- archinstall/tui/help.py | 103 ++- archinstall/tui/menu_item.py | 316 +++++--- uv.lock | 739 +++++++++++++++++++ 10 files changed, 1305 insertions(+), 470 deletions(-) create mode 100644 uv.lock diff --git a/archinstall/__init__.py b/archinstall/__init__.py index 6f5d3825..1c655f70 100644 --- a/archinstall/__init__.py +++ b/archinstall/__init__.py @@ -56,16 +56,17 @@ def plugin(f, *args, **kwargs) -> None: # type: ignore[no-untyped-def] plugins[f.__name__] = f -def _check_new_version() -> None: - info("Checking version...") - +def _fetch_arch_db() -> None: + info("Fetching Arch Linux package database...") try: Pacman.run("-Sy") except Exception as e: - debug(f'Failed to perform version check: {e}') - info('Arch Linux mirrors are not reachable. Please check your internet connection') + debug(f'Failed to sync Arch Linux package database: {e}') exit(1) + +def _check_new_version() -> None: + info("Checking version...") upgrade = None try: @@ -85,8 +86,11 @@ def main() -> None: OR straight as a module: python -m archinstall In any case we will be attempting to load the provided script to be run from the scripts/ folder """ - if not arch_config_handler.args.skip_version_check: - _check_new_version() + if not arch_config_handler.args.offline: + _fetch_arch_db() + + if not arch_config_handler.args.skip_version_check: + _check_new_version() script = arch_config_handler.args.script diff --git a/archinstall/lib/global_menu.py b/archinstall/lib/global_menu.py index 84d1ccb1..7d50b09f 100644 --- a/archinstall/lib/global_menu.py +++ b/archinstall/lib/global_menu.py @@ -37,7 +37,7 @@ from .models.users import User from .output import FormattedOutput from .profile.profile_menu import ProfileConfiguration from .translationhandler import Language, translation_handler -from .utils.util import format_cols, get_password +from .utils.util import get_password if TYPE_CHECKING: from collections.abc import Callable @@ -319,7 +319,8 @@ class GlobalMenu(AbstractMenu): def _prev_additional_pkgs(self, item: MenuItem) -> str | None: if item.value: - return format_cols(item.value, None) + output = '\n'.join(sorted(item.value)) + return output return None def _prev_additional_repos(self, item: MenuItem) -> str | None: diff --git a/archinstall/lib/interactions/general_conf.py b/archinstall/lib/interactions/general_conf.py index 713ecd66..7629b2c3 100644 --- a/archinstall/lib/interactions/general_conf.py +++ b/archinstall/lib/interactions/general_conf.py @@ -3,13 +3,14 @@ from __future__ import annotations from pathlib import Path from typing import TYPE_CHECKING -from archinstall.tui import Alignment, EditMenu, FrameProperties, MenuItem, MenuItemGroup, Orientation, ResultType, SelectMenu, Tui +from archinstall.lib.models.gen import Repository +from archinstall.lib.packages import list_available_packages +from archinstall.tui import Alignment, EditMenu, FrameProperties, MenuItem, MenuItemGroup, Orientation, PreviewStyle, ResultType, SelectMenu, Tui -from ..args import arch_config_handler from ..locale import list_timezones from ..models.audio_configuration import Audio, AudioConfiguration +from ..models.gen import AvailablePackage from ..output import warn -from ..packages.packages import validate_package_list from ..translationhandler import Language if TYPE_CHECKING: @@ -163,40 +164,48 @@ def select_archinstall_language(languages: list[Language], preset: Language) -> raise ValueError('Language selection not handled') -def ask_additional_packages_to_install(preset: list[str] = []) -> list[str]: +def ask_additional_packages_to_install( + preset: list[str] = [], + repositories: set[Repository] = set() +) -> list[str]: + Tui.print('Loading packages...', clear_screen=True) + + repositories |= {Repository.Core, Repository.Extra} + packages = list_available_packages(tuple(repositories)) + # Additional packages (with some light weight error handling for invalid package names) header = str(_('Only packages such as base, base-devel, linux, linux-firmware, efibootmgr and optional profile packages are installed.')) + '\n' header += str(_('If you desire a web browser, such as firefox or chromium, you may specify it in the following prompt.')) + '\n' - header += str(_('Write additional packages to install (space separated, leave blank to skip)')) + header += str(_('Write additional packages to install (space separated, leave blank to skip)')) + '\n' - def validator(value: str) -> str | None: - packages = value.split() if value else [] + # there are over 15k packages so this needs to be quick + preset_packages = [] + for p in preset: + if p in packages: + preset_packages.append(packages[p]) - if len(packages) == 0: - return None + items = [ + MenuItem( + name, + value=pkg, + preview_action=lambda x: x.value.info() + ) for name, + pkg in packages.items() + ] + group = MenuItemGroup(items, sort_items=True) + group.set_selected_by_value(preset_packages) - if arch_config_handler.args.offline or arch_config_handler.args.no_pkg_lookups: - return None - - # Verify packages that were given - out = str(_("Verifying that additional packages exist (this might take a few seconds)")) - Tui.print(out, 0) - _valid, invalid = validate_package_list(packages) - - if invalid: - return f'{_("Some packages could not be found in the repository")}: {invalid}' - - return None - - result = EditMenu( - str(_('Additional packages')), - alignment=Alignment.CENTER, - allow_skip=True, + result = SelectMenu( + group, + header=header, + alignment=Alignment.LEFT, allow_reset=True, - edit_width=100, - validator=validator, - default_text=' '.join(preset) - ).input() + allow_skip=True, + multi=True, + preview_frame=FrameProperties.max('Package info'), + preview_style=PreviewStyle.RIGHT, + preview_size='auto' + ).run() match result.type_: case ResultType.Skip: @@ -204,8 +213,8 @@ def ask_additional_packages_to_install(preset: list[str] = []) -> list[str]: case ResultType.Reset: return [] case ResultType.Selection: - packages = result.text() - return packages.split(' ') + selected_pacakges: list[AvailablePackage] = result.get_values() + return [pkg.name for pkg in selected_pacakges] def add_number_of_parallel_downloads(preset: int | None = None) -> int | None: diff --git a/archinstall/lib/models/gen.py b/archinstall/lib/models/gen.py index 6d626f32..9d3b3d0f 100644 --- a/archinstall/lib/models/gen.py +++ b/archinstall/lib/models/gen.py @@ -1,6 +1,32 @@ from dataclasses import dataclass +from enum import Enum +from functools import cached_property from typing import Any, override +from pydantic import BaseModel + + +class Repository(Enum): + Core = 'core' + Extra = 'extra' + Multilib = 'multilib' + Testing = 'testing' + + def get_repository_list(self) -> list[str]: + match self: + case Repository.Core: + return [Repository.Core.value] + case Repository.Extra: + return [Repository.Extra.value] + case Repository.Multilib: + return [Repository.Multilib.value] + case Repository.Testing: + return [ + 'core-testing', + 'extra-testing', + 'multilib-testing' + ] + @dataclass class PackageSearchResult: @@ -73,9 +99,9 @@ class PackageSearch: ) -@dataclass -class LocalPackage: +class LocalPackage(BaseModel): name: str + repository: str version: str description: str architecture: str @@ -97,16 +123,46 @@ class LocalPackage: validated_by: str provides: str - @property - def pkg_version(self) -> str: - return self.version - @override def __eq__(self, other: object) -> bool: if not isinstance(other, LocalPackage): return NotImplemented - return self.pkg_version == other.pkg_version + return self.version == other.version def __lt__(self, other: 'LocalPackage') -> bool: - return self.pkg_version < other.pkg_version + return self.version < other.version + + +class AvailablePackage(BaseModel): + name: str + architecture: str + build_date: str + depends_on: str + description: str + download_size: str + groups: str + installed_size: str + licenses: str + optional_deps: str + packager: str + provides: str + replaces: str + repository: str + url: str + validated_by: str + version: str + + @cached_property + def longest_key(self) -> int: + return max(len(key) for key in self.dict().keys()) + + # return all package info line by line + def info(self) -> str: + output = '' + for key, value in self.dict().items(): + key = key.replace('_', ' ').capitalize() + key = key.ljust(self.longest_key) + output += f'{key} : {value}\n' + + return output diff --git a/archinstall/lib/packages/__init__.py b/archinstall/lib/packages/__init__.py index 4d5dbf49..51394313 100644 --- a/archinstall/lib/packages/__init__.py +++ b/archinstall/lib/packages/__init__.py @@ -1 +1 @@ -from .packages import find_package, find_packages, group_search, installed_package, package_search, validate_package_list +from .packages import find_package, find_packages, group_search, installed_package, list_available_packages, package_search, validate_package_list diff --git a/archinstall/lib/packages/packages.py b/archinstall/lib/packages/packages.py index ff8fb9fb..929fba52 100644 --- a/archinstall/lib/packages/packages.py +++ b/archinstall/lib/packages/packages.py @@ -1,15 +1,19 @@ -import dataclasses import json import ssl +from functools import lru_cache +from typing import TypeVar from urllib.error import HTTPError from urllib.parse import urlencode from urllib.request import urlopen from urllib.response import addinfourl from ..exceptions import PackageError, SysCallError -from ..models.gen import LocalPackage, PackageSearch, PackageSearchResult +from ..models.gen import AvailablePackage, LocalPackage, PackageSearch, PackageSearchResult, Repository from ..pacman import Pacman +PackageType = TypeVar("PackageType", AvailablePackage, LocalPackage) + + BASE_URL_PKG_SEARCH = 'https://archlinux.org/packages/search/json/' # BASE_URL_PKG_CONTENT = 'https://archlinux.org/packages/search/json/' BASE_GROUP_URL = 'https://archlinux.org/groups/search/json/' @@ -103,16 +107,52 @@ def validate_package_list(packages: list[str]) -> tuple[list[str], list[str]]: return list(valid_packages), list(invalid_packages) -def installed_package(package: str) -> LocalPackage: - package_info = {} +def installed_package(package: str) -> LocalPackage | None: + package_info = [] try: - for line in Pacman.run(f"-Q --info {package}"): - if b':' in line: - key, value = line.decode().split(':', 1) - package_info[key.strip().lower().replace(' ', '_')] = value.strip() + package_info = Pacman.run(f'-Q --info {package}').decode().split('\n') + return _parse_package_output(package_info, LocalPackage) except SysCallError: pass - return LocalPackage( # pylint: disable=no-value-for-parameter - {field.name: package_info.get(field.name) for field in dataclasses.fields(LocalPackage)} # type: ignore - ) + return None + + +@lru_cache +def list_available_packages( + repositories: tuple[Repository] +) -> dict[str, AvailablePackage]: + """ + Returns a list of all available packages in the database + """ + packages: dict[str, AvailablePackage] = {} + current_package: list[str] = [] + filtered_repos = [name for repo in repositories for name in repo.get_repository_list()] + + for line in Pacman.run('-S --info'): + dec_line = line.decode().strip() + current_package.append(dec_line) + + if dec_line.startswith('Validated'): + if current_package: + avail_pkg = _parse_package_output(current_package, AvailablePackage) + if avail_pkg.repository in filtered_repos: + packages[avail_pkg.name] = avail_pkg + current_package = [] + + return packages + + +def _parse_package_output( + package_meta: list[str], + cls: type[PackageType] +) -> PackageType: + package = {} + + for line in package_meta: + if ':' in line: + key, value = line.split(':', 1) + key = key.strip().lower().replace(' ', '_') + package[key] = value.strip() + + return cls.model_validate(package) diff --git a/archinstall/tui/curses_menu.py b/archinstall/tui/curses_menu.py index e588ee03..6247d613 100644 --- a/archinstall/tui/curses_menu.py +++ b/archinstall/tui/curses_menu.py @@ -2,20 +2,18 @@ from __future__ import annotations import curses import curses.panel -import dataclasses import os import signal import sys from abc import ABCMeta, abstractmethod from collections.abc import Callable from curses.textpad import Textbox -from dataclasses import dataclass from types import FrameType, TracebackType from typing import TYPE_CHECKING, Literal, override from ..lib.output import debug from .help import Help -from .menu_item import MenuItem, MenuItemGroup +from .menu_item import MenuItem, MenuItemGroup, MenuItemsState from .types import ( SCROLL_INTERVAL, STYLE, @@ -23,7 +21,6 @@ from .types import ( Chars, FrameProperties, FrameStyle, - MenuCell, MenuKeys, Orientation, PreviewStyle, @@ -121,7 +118,6 @@ class AbstractCurses(metaclass=ABCMeta): return full_header -@dataclass class AbstractViewport: def __init__(self) -> None: pass @@ -397,24 +393,6 @@ class EditViewport(AbstractViewport): self._textbox.edit(self.process_key) # type: ignore[arg-type] -@dataclass -class ViewportState: - cur_pos: int - displayed_entries: list[ViewportEntry] - scroll_pct: int | None - scroll_pos: int | None = 0 - - def offset(self) -> int: - return min([entry.row for entry in self.displayed_entries], default=0) - - def get_rows(self) -> list[int]: - rows = set() - for entry in self.displayed_entries: - rows.add(entry.row) - return list(rows) - - -@dataclass class Viewport(AbstractViewport): def __init__( self, @@ -440,8 +418,6 @@ class Viewport(AbstractViewport): self._main_win.nodelay(False) self._main_win.standout() - self._state: ViewportState | None = None - def getch(self) -> int: return self._main_win.getch() @@ -451,12 +427,13 @@ class Viewport(AbstractViewport): def update( self, - lines: list[ViewportEntry], + entries: list[ViewportEntry], cur_pos: int = 0, scroll_pos: int | None = None ) -> None: - self._state = self._get_viewport_state(lines, cur_pos, scroll_pos) - visible_entries = self._adjust_entries_row(self._state.displayed_entries) + # self._state = self._get_viewport_state(lines, cur_pos, scroll_pos) + # visible_entries = self._adjust_entries_row(self._state.displayed_entries) + visible_entries = entries if self._frame: visible_entries = self.add_frame( @@ -464,7 +441,7 @@ class Viewport(AbstractViewport): self.width, self.height, frame=self._frame, - scroll_pct=self._state.scroll_pct + scroll_pct=scroll_pos ) x_offset = 0 @@ -484,121 +461,6 @@ class Viewport(AbstractViewport): self._main_win.refresh() - def _get_available_screen_rows(self) -> int: - y_offset = 3 if self._frame else 0 - return self.height - y_offset - - def _calc_scroll_percent( - self, total: int, - available_rows: int, - scroll_pos: int - ) -> int | None: - if total <= available_rows: - return None - - percentage = int(scroll_pos / total * 100) - - if percentage + SCROLL_INTERVAL > 100: - percentage = 100 - - return percentage - - def _get_viewport_state( - self, - entries: list[ViewportEntry], - cur_pos: int, - scroll_pos: int | None = 0 - ) -> ViewportState: - if not entries: - return ViewportState(cur_pos, [], 0) - - # we will be checking if the cursor pos is in the same window - # of rows as the previous selection, in that case we can keep - # the currently shown entries to prevent weird moving in long lists - if self._state is not None and scroll_pos is None: - rows = self._state.get_rows() - - if cur_pos in rows: - same_row_entries = [entry for entry in entries if entry.row in rows] - return ViewportState( - cur_pos, - same_row_entries, - self._state.scroll_pct - ) - - total_rows = max([e.row for e in entries]) + 1 # rows start with 0 so add 1 for the count - screen_rows = self._get_available_screen_rows() - visible_entries = self._get_visible_entries( - entries, - cur_pos, - screen_rows, - scroll_pos, - total_rows - ) - - if scroll_pos is not None: - percentage = self._calc_scroll_percent(total_rows, screen_rows, scroll_pos) - else: - percentage = None - - return ViewportState( - cur_pos, - visible_entries, - percentage, - scroll_pos - ) - - def _get_visible_entries( - self, - entries: list[ViewportEntry], - cur_pos: int, - screen_rows: int, - scroll_pos: int | None, - total_rows: int - ) -> list[ViewportEntry]: - if scroll_pos is not None: - if total_rows <= screen_rows: - start = 0 - end = total_rows - else: - start = scroll_pos - end = scroll_pos + screen_rows - else: - if total_rows <= screen_rows: - start = 0 - end = total_rows - else: - if self._state is None: - if cur_pos < screen_rows: - start = 0 - end = screen_rows - else: - start = cur_pos - screen_rows + 1 - end = cur_pos + 1 - else: - if cur_pos < self._state.cur_pos: - start = cur_pos - end = cur_pos + screen_rows - else: - start = cur_pos - screen_rows + 1 - end = cur_pos + 1 - - return [entry for entry in entries if start <= entry.row < end] - - def _adjust_entries_row(self, entries: list[ViewportEntry]) -> list[ViewportEntry]: - assert self._state is not None - modified = [] - - for entry in entries: - mod = dataclasses.replace(entry) - mod.row = entry.row - self._state.offset() - modified.append(mod) - - return modified - - def _unique_rows(self, entries: list[ViewportEntry]) -> int: - return len(set([e.row for e in entries])) - class EditMenu(AbstractCurses): def __init__( @@ -850,6 +712,7 @@ class SelectMenu(AbstractCurses): self._header = header header_offset = self._get_header_offset(header) + self._headers = self.get_header_entries(header, offset=header_offset) if self._interrupt_warning is None: @@ -860,9 +723,7 @@ class SelectMenu(AbstractCurses): else: self._horizontal_cols = 1 - self._row_entries: list[list[MenuCell]] = [] self._prev_scroll_pos: int = 0 - self._cur_pos: int | None = None self._visible_entries: list[ViewportEntry] = [] self._max_height, self._max_width = Tui.t().max_yx @@ -875,6 +736,14 @@ class SelectMenu(AbstractCurses): self._init_viewports(preview_size) + assert self._menu_vp is not None + self._items_state: MenuItemsState = MenuItemsState( + self._item_group, + total_cols=self._horizontal_cols, + total_rows=self._menu_vp.height, + with_frame=self._frame is not None + ) + def _get_header_offset(self, header: str | None) -> int: # WARNING: any changes here will impact the list manager table view if self._orientation == Orientation.HORIZONTAL: @@ -883,7 +752,7 @@ class SelectMenu(AbstractCurses): lines = header.split('\n') if header else [] table_header = [line for line in lines if '|' in line] longest_header = len(table_header[0]) if table_header else 0 - longest_entry = self._item_group.max_width + longest_entry = self._item_group.get_max_width() delta = abs(longest_header - longest_entry) offset = delta + 3 # 3 because it seems to align it... @@ -1050,7 +919,7 @@ class SelectMenu(AbstractCurses): if preview_size == 'auto': match self._preview_style: case PreviewStyle.RIGHT: - menu_width = self._item_group.max_width + 5 + menu_width = self._item_group.get_max_width() + 5 if self._multi: menu_width += 5 prev_size = self._max_width - menu_width @@ -1074,8 +943,8 @@ class SelectMenu(AbstractCurses): def _draw(self) -> None: footer_entries = self._footer_entries() - vp_entries = self._get_row_entries() - self._cur_pos = self._get_cursor_pos() + items = self._items_state.get_view_items() + vp_entries = self._item_to_vp_entry(items) if self._help_vp: self._update_viewport(self._help_vp, [self.help_entry()]) @@ -1084,11 +953,7 @@ class SelectMenu(AbstractCurses): self._update_viewport(self._header_vp, self._headers) if self._menu_vp: - self._update_viewport( - self._menu_vp, - vp_entries, - cur_pos=self._cur_pos - ) + self._update_viewport(self._menu_vp, vp_entries) if vp_entries: self._update_preview() @@ -1109,21 +974,8 @@ class SelectMenu(AbstractCurses): else: viewport.update([]) - def _get_cursor_pos(self) -> int: - for idx, cells in enumerate(self._row_entries): - for cell in cells: - if self._item_group.focus_item == cell.item: - return idx - return 0 - - def _get_visible_items(self) -> list[MenuItem]: - return [it for it in self._item_group.items if self._item_group.should_enable_item(it)] - - def _list_to_cols(self, items: list[MenuItem], cols: int) -> list[list[MenuItem]]: - return [items[i:i + cols] for i in range(0, len(items), cols)] - - def _get_col_widths(self) -> list[int]: - cols_widths = self._calc_col_widths(self._row_entries, self._horizontal_cols) + def _get_col_widths(self, items: list[list[MenuItem]]) -> list[int]: + cols_widths = self._calc_col_widths(items, self._horizontal_cols) return [col_width + len(self._cursor_char) + self._item_distance() for col_width in cols_widths] def _item_distance(self) -> int: @@ -1132,68 +984,56 @@ class SelectMenu(AbstractCurses): else: return self._column_spacing - def _get_row_entries(self) -> list[ViewportEntry]: - cells = self._assemble_menu_cells() + def _item_to_vp_entry(self, items: list[list[MenuItem]]) -> list[ViewportEntry]: entries = [] + cols_widths = self._get_col_widths(items) - self._row_entries = [cells[x:x + self._horizontal_cols] for x in range(0, len(cells), self._horizontal_cols)] - cols_widths = self._get_col_widths() - - for row_idx, row in enumerate(self._row_entries): + for row_idx, row in enumerate(items): cur_pos = len(self._cursor_char) for col_idx, cell in enumerate(row): cur_text = '' style = STYLE.NORMAL - if cell.item == self._item_group.focus_item: + if cell == self._item_group.focus_item: cur_text = self._cursor_char style = STYLE.MENU_STYLE entries += [ViewportEntry(cur_text, row_idx, cur_pos - len(self._cursor_char), STYLE.CURSOR_STYLE)] - entries += [ViewportEntry(cell.text, row_idx, cur_pos, style)] - cur_pos += len(cell.text) + menu_item_text = self._menu_item_text(cell) + entries += [ViewportEntry(menu_item_text, row_idx, cur_pos, style)] + cur_pos += len(menu_item_text) if col_idx < len(row) - 1: - spacer_len = cols_widths[col_idx] - len(cell.text) + spacer_len = cols_widths[col_idx] - len(menu_item_text) entries += [ViewportEntry(' ' * spacer_len, row_idx, cur_pos, STYLE.NORMAL)] cur_pos += spacer_len return entries - def _calc_col_widths( - self, - row_chunks: list[list[MenuCell]], - cols: int - ) -> list[int]: + def _calc_col_widths(self, rows: list[list[MenuItem]], columns: int) -> list[int]: col_widths = [] - for col in range(cols): + + for row in rows: col_entries = [] - for row in row_chunks: - if col < len(row): - col_entries += [len(row[col].text)] + for column in range(columns): + if column < len(row): + col_entries += [len(row[column].text)] if col_entries: - col_widths += [max(col_entries) if col_entries else 0] + col_widths += [max(col_entries)] return col_widths - def _assemble_menu_cells(self) -> list[MenuCell]: - items = self._get_visible_items() - entries = [] + def _menu_item_text(self, item: MenuItem) -> str: + item_text = '' - for item in items: - item_text = '' + if self._multi and not item.is_empty(): + item_text += self._multi_prefix(item) - if self._multi and not item.is_empty(): - item_text += self._multi_prefix(item) - - item_text += self._item_group.get_item_text(item) - - entries += [MenuCell(item, item_text)] - - return entries + item_text += self._item_group.get_item_text(item) + return item_text def _update_preview(self) -> None: if not self._preview_vp: @@ -1214,15 +1054,64 @@ class SelectMenu(AbstractCurses): preview_text = action_text.split('\n') entries = [ViewportEntry(e, idx, 0, STYLE.NORMAL) for idx, e in enumerate(preview_text)] - self._calc_prev_scroll_pos(entries) + total_prev_rows = max([e.row for e in entries]) + 1 # rows start with 0 and we need the count + available_rows = self._preview_vp.height - 2 # for the preview frame - self._preview_vp.update(entries, scroll_pos=self._prev_scroll_pos) + self._calc_prev_scroll_pos(entries, total_prev_rows) + prev_entries = self._get_scroll_win_prev_entries(entries, total_prev_rows, available_rows) + scroll_pct = self._get_scroll_pct(total_prev_rows, available_rows) - def _calc_prev_scroll_pos(self, entries: list[ViewportEntry]) -> None: - total_rows = max([e.row for e in entries]) + 1 # rows start with 0 and we need the count + self._preview_vp.update(prev_entries, scroll_pos=scroll_pct) - if self._prev_scroll_pos >= total_rows: - self._prev_scroll_pos = total_rows - 2 + def _get_scroll_pct( + self, + total_prev_rows: int, + available_rows: int + ) -> int | None: + assert self._preview_vp is not None + + if total_prev_rows <= available_rows: + return None + + pct = int(self._prev_scroll_pos / total_prev_rows * 100) + + if pct + SCROLL_INTERVAL > 100: + pct = 100 + + if pct < 0: + pct = 0 + + return pct + + def _get_scroll_win_prev_entries( + self, + entries: list[ViewportEntry], + total_prev_rows: int, + available_rows: int + ) -> list[ViewportEntry]: + assert self._preview_vp is not None + + start_row = self._prev_scroll_pos + end_row = start_row + available_rows + + if end_row > total_prev_rows: + end_row = total_prev_rows + + prev_entries = [e for e in entries if start_row <= e.row < end_row] + + # normalize the rows + for e in prev_entries: + e.row -= start_row + + return prev_entries + + def _calc_prev_scroll_pos( + self, + entries: list[ViewportEntry], + total_prev_rows: int + ) -> None: + if self._prev_scroll_pos >= total_prev_rows: + self._prev_scroll_pos = total_prev_rows - 2 elif self._prev_scroll_pos < 0: self._prev_scroll_pos = 0 @@ -1294,12 +1183,14 @@ class SelectMenu(AbstractCurses): return Result(ResultType.Selection, self._item_group.focus_item) return None - case MenuKeys.MENU_UP | MenuKeys.MENU_DOWN | MenuKeys.MENU_LEFT | MenuKeys.MENU_RIGHT: - self._focus_item(handle) + case MenuKeys.MENU_DOWN | MenuKeys.MENU_RIGHT: + self._focus_item('next') + case MenuKeys.MENU_UP | MenuKeys.MENU_LEFT: + self._focus_item('prev') case MenuKeys.MENU_START: - self._item_group.focus_first() + self._focus_item('first') case MenuKeys.MENU_END: - self._item_group.focus_last() + self._focus_item('last') case MenuKeys.MULTI_SELECT: if self._multi: self._item_group.select_current_item() @@ -1315,7 +1206,7 @@ class SelectMenu(AbstractCurses): if self._allow_skip: return Result(ResultType.Skip, None) case MenuKeys.NUM_KEYS: - self._item_group.set_focus_item_index(key - 49) + self._item_group.focus_index(key - 49) case MenuKeys.SCROLL_DOWN: self._prev_scroll_pos += SCROLL_INTERVAL case MenuKeys.SCROLL_UP: @@ -1323,56 +1214,22 @@ class SelectMenu(AbstractCurses): case _: pass - self._draw() return None - def _focus_item(self, key: MenuKeys) -> None: - focus_item = self._item_group.focus_item - next_row = 0 - next_col = 0 + def _focus_item(self, direction: Literal['next' | 'prev' | 'first' | 'last']) -> None: + # reset the preview scroll as the newly focused item + # may have a different preview row count and it'll blow up + self._prev_scroll_pos = 0 - for row_idx, row in enumerate(self._row_entries): - for col_idx, cell in enumerate(row): - if cell.item == focus_item: - match key: - case MenuKeys.MENU_UP: - next_row = row_idx - 1 - next_col = col_idx - - if next_row < 0: - next_row = len(self._row_entries) - 1 - if next_col >= len(self._row_entries[next_row]): - next_col = len(self._row_entries[next_row]) - 1 - case MenuKeys.MENU_DOWN: - next_row = row_idx + 1 - next_col = col_idx - - if next_row >= len(self._row_entries): - next_row = 0 - if next_col >= len(self._row_entries[next_row]): - next_col = len(self._row_entries[next_row]) - 1 - case MenuKeys.MENU_RIGHT: - next_col = col_idx + 1 - next_row = row_idx - - if next_col >= len(self._row_entries[row_idx]): - next_col = 0 - next_row = 0 if next_row == (len(self._row_entries) - 1) else next_row + 1 - case MenuKeys.MENU_LEFT: - next_col = col_idx - 1 - next_row = row_idx - - if next_col < 0: - next_row = len(self._row_entries) - 1 if next_row == 0 else next_row - 1 - next_col = len(self._row_entries[next_row]) - 1 - - if next_row < len(self._row_entries): - row = self._row_entries[next_row] - if next_col < len(row): - self._item_group.focus_item = row[next_col].item - - if self._item_group.focus_item and self._item_group.focus_item.is_empty(): - self._focus_item(key) + match direction: + case 'next': + self._item_group.focus_next() + case 'prev': + self._item_group.focus_prev() + case 'first': + self._item_group.focus_first() + case 'last': + self._item_group.focus_last() class Tui: @@ -1447,10 +1304,10 @@ class Tui: endl: str | None = '\n', clear_screen: bool = False ) -> None: - if Tui._t is None: - if clear_screen: - os.system('clear') + if clear_screen: + os.system('clear') + if Tui._t is None: print(text, end=endl) sys.stdout.flush() diff --git a/archinstall/tui/help.py b/archinstall/tui/help.py index 2761151e..f0ebbb3d 100644 --- a/archinstall/tui/help.py +++ b/archinstall/tui/help.py @@ -1,5 +1,12 @@ +from collections.abc import Callable from dataclasses import dataclass, field from enum import Enum +from typing import TYPE_CHECKING + +from archinstall.lib.translationhandler import DeferredTranslation + +if TYPE_CHECKING: + _: Callable[[str], DeferredTranslation] class HelpTextGroupId(Enum): @@ -28,52 +35,70 @@ class HelpGroup: class Help: - general = HelpGroup( - group_id=HelpTextGroupId.GENERAL, - group_entries=[ - HelpText('Show help', ['Ctrl+h']), - HelpText('Exit help', ['Esc']), - ] - ) + # the groups needs to be classmethods not static methods + # because they rely on the DeferredTranslation setup first; + # if they are static methods, they will be called before the + # translation setup is done - navigation = HelpGroup( - group_id=HelpTextGroupId.NAVIGATION, - group_entries=[ - HelpText('Preview scroll up', ['PgUp']), - HelpText('Preview scroll down', ['PgDown']), - HelpText('Move up', ['k', '↑']), - HelpText('Move down', ['j', '↓']), - HelpText('Move right', ['l', '→']), - HelpText('Move left', ['h', '←']), - HelpText('Jump to entry', ['1..9']) - ] - ) + @staticmethod + def general() -> HelpGroup: + return HelpGroup( + group_id=HelpTextGroupId.GENERAL, + group_entries=[ + HelpText(str(_('Show help')), ['Ctrl+h']), + HelpText(str(_('Exit help')), ['Esc']), + ] + ) - selection = HelpGroup( - group_id=HelpTextGroupId.SELECTION, - group_entries=[ - HelpText('Skip selection (if available)', ['Esc']), - HelpText('Reset selection (if available)', ['Ctrl+c']), - HelpText('Select on single select', ['Enter']), - HelpText('Select on select', ['Space', 'Tab']), - HelpText('Reset', ['Ctrl-C']), - HelpText('Skip selection menu', ['Esc']), - ] - ) + @staticmethod + def navigation() -> HelpGroup: + return HelpGroup( + group_id=HelpTextGroupId.NAVIGATION, + group_entries=[ + HelpText(str(_('Preview scroll up')), ['PgUp']), + HelpText(str(_('Preview scroll down')), ['PgDown']), + HelpText(str(_('Move up')), ['k', '↑']), + HelpText(str(_('Move down')), ['j', '↓']), + HelpText(str(_('Move right')), ['l', '→']), + HelpText(str(_('Move left')), ['h', '←']), + HelpText(str(_('Jump to entry')), ['1..9']) + ] + ) - search = HelpGroup( - group_id=HelpTextGroupId.SEARCH, - group_entries=[ - HelpText('Start search mode', ['/']), - HelpText('Exit search mode', ['Esc']), - ] - ) + @staticmethod + def selection() -> HelpGroup: + return HelpGroup( + group_id=HelpTextGroupId.SELECTION, + group_entries=[ + HelpText(str(_('Skip selection (if available)')), ['Esc']), + HelpText(str(_('Reset selection (if available)')), ['Ctrl+c']), + HelpText(str(_('Select on single select')), ['Enter']), + HelpText(str(_('Select on multi select')), ['Space', 'Tab']), + HelpText(str(_('Reset')), ['Ctrl-C']), + HelpText(str(_('Skip selection menu')), ['Esc']), + ] + ) + + @staticmethod + def search() -> HelpGroup: + return HelpGroup( + group_id=HelpTextGroupId.SEARCH, + group_entries=[ + HelpText(str(_('Start search mode')), ['/']), + HelpText(str(_('Exit search mode')), ['Esc']), + ] + ) @staticmethod def get_help_text() -> str: help_output = '' - help_texts = [Help.general, Help.navigation, Help.selection, Help.search] - max_desc_width = max([help.get_desc_width() for help in help_texts]) + help_texts = [ + Help.general(), + Help.navigation(), + Help.selection(), + Help.search(), + ] + max_desc_width = max([help.get_desc_width() for help in help_texts]) + 2 max_key_width = max([help.get_key_width() for help in help_texts]) for help_group in help_texts: diff --git a/archinstall/tui/menu_item.py b/archinstall/tui/menu_item.py index 7e6dcce4..03978186 100644 --- a/archinstall/tui/menu_item.py +++ b/archinstall/tui/menu_item.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Callable from dataclasses import dataclass, field +from functools import cached_property from typing import TYPE_CHECKING, Any, ClassVar from ..lib.output import unicode_ljust @@ -66,33 +67,36 @@ class MenuItem: return None -@dataclass class MenuItemGroup: - menu_items: list[MenuItem] - focus_item: MenuItem | None = None - default_item: MenuItem | None = None - selected_items: list[MenuItem] = field(default_factory=list) - sort_items: bool = False - checkmarks: bool = False - - _filter_pattern: str = '' - - def __post_init__(self) -> None: - if len(self.menu_items) < 1: + def __init__( + self, + menu_items: list[MenuItem], + focus_item: MenuItem | None = None, + default_item: MenuItem | None = None, + sort_items: bool = False, + checkmarks: bool = False + ) -> None: + if len(menu_items) < 1: raise ValueError('Menu must have at least one item') - if self.sort_items: - self.menu_items = sorted(self.menu_items, key=lambda x: x.text) + if sort_items: + menu_items = sorted(menu_items, key=lambda x: x.text) - if not self.focus_item: - if self.selected_items: - self.focus_item = self.selected_items[0] - else: - self.focus_item = self.menu_items[0] + if not focus_item: + focus_item = menu_items[0] - if self.focus_item not in self.menu_items: + if focus_item not in menu_items: raise ValueError('Selected item not in menu') + self.menu_items: list[MenuItem] = menu_items + self.focus_item: MenuItem = focus_item + self.selected_items: list[MenuItem] = [] + self.default_item: MenuItem | None = default_item + + self._checkmarks: bool = checkmarks + + self._filter_pattern: str = '' + def find_by_key(self, key: str) -> MenuItem: for item in self.menu_items: if item.key == key: @@ -100,6 +104,9 @@ class MenuItemGroup: raise ValueError(f'No key found for: {key}') + def get_enabled_items(self) -> list[MenuItem]: + return [it for it in self.items if self.is_enabled(it)] + @staticmethod def yes_no() -> 'MenuItemGroup': return MenuItemGroup( @@ -137,39 +144,30 @@ class MenuItemGroup: if values: self.set_focus_by_value(values[0]) - def index_of(self, item: MenuItem) -> int: - return self.items.index(item) + def index_focus(self) -> int | None: + if self.focus_item and self.items: + return self.items.index(self.focus_item) - def index_focus(self) -> int: - if self.focus_item: - return self.index_of(self.focus_item) - - raise ValueError('No focus item set') - - def index_last(self) -> int: - return self.index_of(self.items[-1]) - - def index_first(self) -> int: - return self.index_of(self.items[0]) + return None @property def size(self) -> int: return len(self.items) - @property - def max_width(self) -> int: + def get_max_width(self) -> int: # use the menu_items not the items here otherwise the preview # will get resized all the time when a filter is applied return max([len(self.get_item_text(item)) for item in self.menu_items]) - def _max_item_width(self) -> int: + @cached_property + def _max_items_text_width(self) -> int: return max([len(item.text) for item in self.menu_items]) def get_item_text(self, item: MenuItem) -> str: if item.is_empty(): return '' - max_width = self._max_item_width() + max_width = self._max_items_text_width display_text = item.get_display_value() default_text = self._default_suffix(item) @@ -178,7 +176,7 @@ class MenuItemGroup: if display_text: text = f'{text}{spacing}{display_text}' - elif self.checkmarks: + elif self._checkmarks: from .types import Chars if item.has_value(): @@ -197,42 +195,38 @@ class MenuItemGroup: return str(_(' (default)')) return '' - @property + @cached_property def items(self) -> list[MenuItem]: - f = self._filter_pattern.lower() - items = filter(lambda item: item.is_empty() or f in item.text.lower(), self.menu_items) + _filter = self._filter_pattern.lower() + items = filter(lambda item: item.is_empty() or _filter in item.text.lower(), self.menu_items) return list(items) @property def filter_pattern(self) -> str: return self._filter_pattern + def has_filter(self) -> bool: + return self._filter_pattern != '' + def set_filter_pattern(self, pattern: str) -> None: self._filter_pattern = pattern - self.reload_focus_itme() + delattr(self, 'items') # resetting the cache + self._reload_focus_item() def append_filter(self, pattern: str) -> None: self._filter_pattern += pattern - self.reload_focus_itme() + delattr(self, 'items') # resetting the cache + self._reload_focus_item() def reduce_filter(self) -> None: self._filter_pattern = self._filter_pattern[:-1] - self.reload_focus_itme() + delattr(self, 'items') # resetting the cache + self._reload_focus_item() - def set_focus_item_index(self, index: int) -> None: - items = self.items - non_empty_items = [item for item in items if not item.is_empty()] - if index < 0 or index >= len(non_empty_items): - return - - for item in non_empty_items[index:]: - if not item.is_empty(): - self.focus_item = item - return - - def reload_focus_itme(self) -> None: - if self.focus_item not in self.items: - self.focus_first() + def _reload_focus_item(self) -> None: + if len(self.items) > 0: + if self.focus_item not in self.items: + self.focus_first() def is_item_selected(self, item: MenuItem) -> bool: return item in self.selected_items @@ -244,67 +238,71 @@ class MenuItemGroup: else: self.selected_items.append(self.focus_item) - def is_focused(self, item: MenuItem) -> bool: - if isinstance(self.focus_item, list): - return item in self.focus_item - else: - return item == self.focus_item - - def _first(self, items: list[MenuItem], ignore_empty: bool) -> MenuItem | None: - for item in items: - if not ignore_empty: - return item - - if not item.is_empty(): - return item - - return None - - def get_first_item(self, ignore_empty: bool = True) -> MenuItem | None: - return self._first(self.items, ignore_empty) - - def get_last_item(self, ignore_empty: bool = True) -> MenuItem | None: - items = self.items - rev_items = list(reversed(items)) - return self._first(rev_items, ignore_empty) + def focus_index(self, index: int) -> None: + enabled = self.get_enabled_items() + self.focus_item = enabled[index] def focus_first(self) -> None: - first_item = self.get_first_item() - if first_item: + first_item: MenuItem | None = self.items[0] + + if first_item and not self._is_selectable(first_item): + first_item = self._find_next_selectable_item(self.items, first_item, 1) + + if first_item is not None: self.focus_item = first_item def focus_last(self) -> None: - last_item = self.get_last_item() - if last_item: + last_item: MenuItem | None = self.items[-1] + + if last_item and not self._is_selectable(last_item): + last_item = self._find_next_selectable_item(self.items, last_item, -1) + + if last_item is not None: self.focus_item = last_item def focus_prev(self, skip_empty: bool = True) -> None: - items = self.items + assert self.focus_item is not None + item = self._find_next_selectable_item(self.items, self.focus_item, -1) - if self.focus_item not in items: - return + if item is not None: + self.focus_item = item - if self.focus_item == items[0]: - self.focus_item = items[-1] - else: - self.focus_item = items[items.index(self.focus_item) - 1] + def focus_next(self, skip_not_enabled: bool = True) -> None: + assert self.focus_item is not None + item = self._find_next_selectable_item(self.items, self.focus_item, 1) - if self.focus_item.is_empty() and skip_empty: - self.focus_prev(skip_empty) + if item is not None: + self.focus_item = item - def focus_next(self, skip_empty: bool = True) -> None: - items = self.items + def get_focus_index(self) -> int: + return self.items.index(self.focus_item) - if self.focus_item not in items: - return + def _find_next_selectable_item( + self, + items: list[MenuItem], + start_item: MenuItem, + direction: int + ) -> MenuItem | None: + index = self.items.index(start_item) - if self.focus_item == items[-1]: - self.focus_item = items[0] - else: - self.focus_item = items[items.index(self.focus_item) + 1] + start = index + direction + end = 0 - if self.focus_item.is_empty() and skip_empty: - self.focus_next(skip_empty) + if direction == 1: + end = len(items) + index + elif direction == -1: + if index == 0: + end = len(items) * direction + else: + end = index * direction + + for idx in range(start, end, direction): + idx = idx % len(items) + + if self._is_selectable(items[idx]): + return items[idx] + + return None def is_mandatory_fulfilled(self) -> bool: for item in self.menu_items: @@ -318,14 +316,20 @@ class MenuItemGroup: return max(spaces) return 0 - def should_enable_item(self, item: MenuItem) -> bool: + def _is_selectable(self, item: MenuItem) -> bool: + if item.is_empty(): + return False + + return self.is_enabled(item) + + def is_enabled(self, item: MenuItem) -> bool: if not item.enabled: return False for dep in item.dependencies: if isinstance(dep, str): item = self.find_by_key(dep) - if not item.value or not self.should_enable_item(item): + if not item.value or not self.is_enabled(item): return False else: return dep() @@ -336,3 +340,103 @@ class MenuItemGroup: return False return True + + +class MenuItemsState: + def __init__( + self, + item_group: MenuItemGroup, + total_cols: int, + total_rows: int, + with_frame: bool + ) -> None: + self._item_group = item_group + self._total_cols = total_cols + self._total_rows = total_rows - 2 if with_frame else total_rows + + self._prev_row_idx: int = -1 + self._prev_visible_rows: list[int] = [] + self._view_items: list[list[MenuItem]] = [] + + def _determine_foucs_row(self) -> int | None: + focus_index = self._item_group.index_focus() + + if focus_index is None: + return None + + row_index = focus_index // self._total_cols + return row_index + + def get_view_items(self) -> list[list[MenuItem]]: + enabled_items = self._item_group.get_enabled_items() + focus_row_idx = self._determine_foucs_row() + + if focus_row_idx is None: + return [] + + start, end = 0, 0 + + if ( + len(self._view_items) == 0 + or self._prev_row_idx == -1 + or self._item_group.has_filter() + ): # initial setup or filter + if focus_row_idx < self._total_rows: + start = 0 + end = self._total_rows + elif focus_row_idx > len(enabled_items) - self._total_rows: + start = len(enabled_items) - self._total_rows + end = len(enabled_items) + else: + start = focus_row_idx + end = focus_row_idx + self._total_rows + elif len(enabled_items) <= self._total_rows: # the view can handle oll items + start = 0 + end = self._total_rows + elif not self._item_group.has_filter() and focus_row_idx in self._prev_visible_rows: # focus is in the same view + self._prev_row_idx = focus_row_idx + return self._view_items + else: + if self._item_group.has_filter(): + start = focus_row_idx + end = focus_row_idx + self._total_rows + else: + delta = focus_row_idx - self._prev_row_idx + + if delta > 0: # cursor is on the bottom most row + start = focus_row_idx - self._total_rows + 1 + end = focus_row_idx + 1 + else: # focus is on the top most row + start = focus_row_idx + end = focus_row_idx + self._total_rows + + self._view_items = self._get_view_items(enabled_items, start, end) + self._prev_visible_rows = list(range(start, end)) + self._prev_row_idx = focus_row_idx + + return self._view_items + + def _get_view_items( + self, + items: list[MenuItem], + start_row: int, + total_rows: int + ) -> list[list[MenuItem]]: + groups: list[list[MenuItem]] = [] + nr_items = self._total_cols * min(total_rows, len(items)) + + for x in range(start_row, nr_items, self._total_cols): + groups.append( + items[x:x + self._total_cols] + ) + + return groups + + def _max_visible_items(self) -> int: + return self._total_cols * self._total_rows + + def _remaining_next_spots(self) -> int: + return self._max_visible_items() - self._prev_row_idx + + def _remaining_prev_spots(self) -> int: + return self._max_visible_items() - self._remaining_next_spots() diff --git a/uv.lock b/uv.lock new file mode 100644 index 00000000..1ad5eb19 --- /dev/null +++ b/uv.lock @@ -0,0 +1,739 @@ +version = 1 +revision = 1 +requires-python = ">=3.12" + +[[package]] +name = "alabaster" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a6/f8/d9c74d0daf3f742840fd818d69cfae176fa332022fd44e3469487d5a9420/alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e", size = 24210 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929 }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, +] + +[[package]] +name = "archinstall" +source = { editable = "." } +dependencies = [ + { name = "pydantic" }, + { name = "pyparted" }, +] + +[package.optional-dependencies] +dev = [ + { name = "flake8" }, + { name = "mypy" }, + { name = "pre-commit" }, + { name = "pylint" }, + { name = "pylint-pydantic" }, + { name = "pytest" }, + { name = "ruff" }, +] +doc = [ + { name = "sphinx" }, +] +log = [ + { name = "systemd-python" }, +] + +[package.metadata] +requires-dist = [ + { name = "flake8", marker = "extra == 'dev'", specifier = "==7.1.1" }, + { name = "mypy", marker = "extra == 'dev'", specifier = "==1.15.0" }, + { name = "pre-commit", marker = "extra == 'dev'", specifier = "==4.1.0" }, + { name = "pydantic", specifier = "==2.10.6" }, + { name = "pylint", marker = "extra == 'dev'", specifier = "==3.3.4" }, + { name = "pylint-pydantic", marker = "extra == 'dev'", specifier = "==0.3.5" }, + { name = "pyparted", url = "https://github.com//dcantrell/pyparted/archive/v3.13.0.tar.gz" }, + { name = "pytest", marker = "extra == 'dev'", specifier = "==8.3.4" }, + { name = "ruff", marker = "extra == 'dev'", specifier = "==0.9.5" }, + { name = "sphinx", marker = "extra == 'doc'" }, + { name = "systemd-python", marker = "extra == 'log'", specifier = "==235" }, +] +provides-extras = ["log", "dev", "doc"] + +[[package]] +name = "astroid" +version = "3.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/80/c5/5c83c48bbf547f3dd8b587529db7cf5a265a3368b33e85e76af8ff6061d3/astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b", size = 398196 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/28/0bc8a17d6cd4cc3c79ae41b7105a2b9a327c110e5ddd37a8a27b29a5c8a2/astroid-3.3.8-py3-none-any.whl", hash = "sha256:187ccc0c248bfbba564826c26f070494f7bc964fd286b6d9fff4420e55de828c", size = 275153 }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537 }, +] + +[[package]] +name = "certifi" +version = "2025.1.31" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 }, + { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, + { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, + { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, + { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, + { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, + { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, + { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, + { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, + { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, + { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, + { url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 }, + { url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 }, + { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, + { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, + { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, + { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, + { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, + { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, + { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, + { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, + { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, + { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, + { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, + { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, + { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, + { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, +] + +[[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 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, +] + +[[package]] +name = "dill" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/43/86fe3f9e130c4137b0f1b50784dd70a5087b911fe07fa81e53e0c4c47fea/dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c", size = 187000 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a", size = 119418 }, +] + +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, +] + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, +] + +[[package]] +name = "filelock" +version = "3.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/9c/0b15fb47b464e1b663b1acd1253a062aa5feecb07d4e597daea542ebd2b5/filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e", size = 18027 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 }, +] + +[[package]] +name = "flake8" +version = "7.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mccabe" }, + { name = "pycodestyle" }, + { name = "pyflakes" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/37/72/e8d66150c4fcace3c0a450466aa3480506ba2cae7b61e100a2613afc3907/flake8-7.1.1.tar.gz", hash = "sha256:049d058491e228e03e67b390f311bbf88fce2dbaa8fa673e7aea87b7198b8d38", size = 48054 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/42/65004373ac4617464f35ed15931b30d764f53cdd30cc78d5aea349c8c050/flake8-7.1.1-py2.py3-none-any.whl", hash = "sha256:597477df7860daa5aa0fdd84bf5208a043ab96b8e96ab708770ae0364dd03213", size = 57731 }, +] + +[[package]] +name = "identify" +version = "2.6.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/fa/5eb460539e6f5252a7c5a931b53426e49258cde17e3d50685031c300a8fd/identify-2.6.8.tar.gz", hash = "sha256:61491417ea2c0c5c670484fd8abbb34de34cdae1e5f39a73ee65e48e4bb663fc", size = 99249 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/8c/4bfcab2d8286473b8d83ea742716f4b79290172e75f91142bc1534b05b9a/identify-2.6.8-py2.py3-none-any.whl", hash = "sha256:83657f0f766a3c8d0eaea16d4ef42494b39b34629a4b3192a9d020d349b3e255", size = 99109 }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, +] + +[[package]] +name = "imagesize" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + +[[package]] +name = "isort" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/28/b382d1656ac0ee4cef4bf579b13f9c6c813bff8a5cb5996669592c8c75fa/isort-6.0.0.tar.gz", hash = "sha256:75d9d8a1438a9432a7d7b54f2d3b45cad9a4a0fdba43617d9873379704a8bdf1", size = 828356 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c7/d6017f09ae5b1206fbe531f7af3b6dac1f67aedcbd2e79f3b386c27955d6/isort-6.0.0-py3-none-any.whl", hash = "sha256:567954102bb47bb12e0fae62606570faacddd441e45683968c8d1734fb1af892", size = 94053 }, +] + +[[package]] +name = "jinja2" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/af/92/b3130cbbf5591acf9ade8708c365f3238046ac7cb8ccba6e81abccb0ccff/jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb", size = 244674 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/0f/2ba5fbcd631e3e88689309dbe978c5769e883e4b84ebfe7da30b43275c5a/jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb", size = 134596 }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348 }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149 }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118 }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993 }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178 }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319 }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352 }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097 }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601 }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274 }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352 }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122 }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085 }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978 }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208 }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357 }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344 }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101 }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603 }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510 }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486 }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480 }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914 }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796 }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473 }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114 }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098 }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208 }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739 }, +] + +[[package]] +name = "mccabe" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350 }, +] + +[[package]] +name = "mypy" +version = "1.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mypy-extensions" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/98/3a/03c74331c5eb8bd025734e04c9840532226775c47a2c39b56a0c8d4f128d/mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd", size = 10793981 }, + { url = "https://files.pythonhosted.org/packages/f0/1a/41759b18f2cfd568848a37c89030aeb03534411eef981df621d8fad08a1d/mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f", size = 9749175 }, + { url = "https://files.pythonhosted.org/packages/12/7e/873481abf1ef112c582db832740f4c11b2bfa510e829d6da29b0ab8c3f9c/mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464", size = 11455675 }, + { url = "https://files.pythonhosted.org/packages/b3/d0/92ae4cde706923a2d3f2d6c39629134063ff64b9dedca9c1388363da072d/mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee", size = 12410020 }, + { url = "https://files.pythonhosted.org/packages/46/8b/df49974b337cce35f828ba6fda228152d6db45fed4c86ba56ffe442434fd/mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e", size = 12498582 }, + { url = "https://files.pythonhosted.org/packages/13/50/da5203fcf6c53044a0b699939f31075c45ae8a4cadf538a9069b165c1050/mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22", size = 9366614 }, + { url = "https://files.pythonhosted.org/packages/6a/9b/fd2e05d6ffff24d912f150b87db9e364fa8282045c875654ce7e32fffa66/mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445", size = 10788592 }, + { url = "https://files.pythonhosted.org/packages/74/37/b246d711c28a03ead1fd906bbc7106659aed7c089d55fe40dd58db812628/mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d", size = 9753611 }, + { url = "https://files.pythonhosted.org/packages/a6/ac/395808a92e10cfdac8003c3de9a2ab6dc7cde6c0d2a4df3df1b815ffd067/mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5", size = 11438443 }, + { url = "https://files.pythonhosted.org/packages/d2/8b/801aa06445d2de3895f59e476f38f3f8d610ef5d6908245f07d002676cbf/mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036", size = 12402541 }, + { url = "https://files.pythonhosted.org/packages/c7/67/5a4268782eb77344cc613a4cf23540928e41f018a9a1ec4c6882baf20ab8/mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357", size = 12494348 }, + { url = "https://files.pythonhosted.org/packages/83/3e/57bb447f7bbbfaabf1712d96f9df142624a386d98fb026a761532526057e/mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf", size = 9373648 }, + { url = "https://files.pythonhosted.org/packages/09/4e/a7d65c7322c510de2c409ff3828b03354a7c43f5a8ed458a7a131b41c7b9/mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e", size = 2221777 }, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/a4/1ab47638b92648243faf97a5aeb6ea83059cc3624972ab6b8d2316078d3f/mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782", size = 4433 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/e2/5d3f6ada4297caebe1a2add3b126fe800c96f56dbe5d1988a2cbe0b267aa/mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d", size = 4695 }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, +] + +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pre-commit" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2a/13/b62d075317d8686071eb843f0bb1f195eb332f48869d3c31a4c6f1e063ac/pre_commit-4.1.0.tar.gz", hash = "sha256:ae3f018575a588e30dfddfab9a05448bfbd6b73d78709617b5a2b853549716d4", size = 193330 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/b3/df14c580d82b9627d173ceea305ba898dca135feb360b6d84019d0803d3b/pre_commit-4.1.0-py2.py3-none-any.whl", hash = "sha256:d29e7cb346295bcc1cc75fc3e92e343495e3ea0196c9ec6ba53f49f10ab6ae7b", size = 220560 }, +] + +[[package]] +name = "pycodestyle" +version = "2.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/aa/210b2c9aedd8c1cbeea31a50e42050ad56187754b34eb214c46709445801/pycodestyle-2.12.1.tar.gz", hash = "sha256:6838eae08bbce4f6accd5d5572075c63626a15ee3e6f842df996bf62f6d73521", size = 39232 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/d8/a211b3f85e99a0daa2ddec96c949cac6824bd305b040571b82a03dd62636/pycodestyle-2.12.1-py2.py3-none-any.whl", hash = "sha256:46f0fb92069a7c28ab7bb558f05bfc0110dac69a0cd23c61ea0040283a9d78b3", size = 31284 }, +] + +[[package]] +name = "pydantic" +version = "2.10.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, +] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, + { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, + { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, + { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, + { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, + { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, + { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, + { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, + { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, + { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, + { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, + { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, + { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, + { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, + { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, + { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, + { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, + { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, + { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, + { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, + { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, + { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, + { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, + { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, + { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, + { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, + { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, + { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, +] + +[[package]] +name = "pyflakes" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/f9/669d8c9c86613c9d568757c7f5824bd3197d7b1c6c27553bc5618a27cce2/pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f", size = 63788 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/d7/f1b7db88d8e4417c5d47adad627a93547f44bdc9028372dbd2313f34a855/pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a", size = 62725 }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, +] + +[[package]] +name = "pylint" +version = "3.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "astroid" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "dill" }, + { name = "isort" }, + { name = "mccabe" }, + { name = "platformdirs" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/b9/50be49afc91469f832c4bf12318ab4abe56ee9aa3700a89aad5359ad195f/pylint-3.3.4.tar.gz", hash = "sha256:74ae7a38b177e69a9b525d0794bd8183820bfa7eb68cc1bee6e8ed22a42be4ce", size = 1518905 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/8b/eef15df5f4e7aa393de31feb96ca9a3d6639669bd59d589d0685d5ef4e62/pylint-3.3.4-py3-none-any.whl", hash = "sha256:289e6a1eb27b453b08436478391a48cd53bb0efb824873f949e709350f3de018", size = 522280 }, +] + +[[package]] +name = "pylint-plugin-utils" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pylint" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4b/d2/3b9728910bc69232ec38d8fb7053c03c887bfe7e6e170649b683dd351750/pylint_plugin_utils-0.8.2.tar.gz", hash = "sha256:d3cebf68a38ba3fba23a873809155562571386d4c1b03e5b4c4cc26c3eee93e4", size = 10674 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/ee/49d11aee31061bcc1d2726bd8334a2883ddcdbde7d7744ed6b3bd11704ed/pylint_plugin_utils-0.8.2-py3-none-any.whl", hash = "sha256:ae11664737aa2effbf26f973a9e0b6779ab7106ec0adc5fe104b0907ca04e507", size = 11171 }, +] + +[[package]] +name = "pylint-pydantic" +version = "0.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "pylint" }, + { name = "pylint-plugin-utils" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/b6/57b898006cb358af02b6a5b84909630630e89b299e7f9fc2dc7b3f0b61ef/pylint_pydantic-0.3.5-py3-none-any.whl", hash = "sha256:e7a54f09843b000676633ed02d5985a4a61c8da2560a3b0d46082d2ff171c4a1", size = 16139 }, +] + +[[package]] +name = "pyparted" +version = "3.13.0" +source = { url = "https://github.com//dcantrell/pyparted/archive/v3.13.0.tar.gz" } +sdist = { hash = "sha256:9d69d822f2679e3b5c8279bb23d2a1b736ff15b34bd95833e317787f73794701" } + +[[package]] +name = "pytest" +version = "8.3.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, +] + +[[package]] +name = "requests" +version = "2.32.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, +] + +[[package]] +name = "roman-numerals-py" +version = "3.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/76/48fd56d17c5bdbdf65609abbc67288728a98ed4c02919428d4f52d23b24b/roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d", size = 9017 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/53/97/d2cbbaa10c9b826af0e10fdf836e1bf344d9f0abb873ebc34d1f49642d3f/roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c", size = 7742 }, +] + +[[package]] +name = "ruff" +version = "0.9.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/74/6c359f6b9ed85b88df6ef31febce18faeb852f6c9855651dfb1184a46845/ruff-0.9.5.tar.gz", hash = "sha256:11aecd7a633932875ab3cb05a484c99970b9d52606ce9ea912b690b02653d56c", size = 3634177 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/4b/82b7c9ac874e72b82b19fd7eab57d122e2df44d2478d90825854f9232d02/ruff-0.9.5-py3-none-linux_armv6l.whl", hash = "sha256:d466d2abc05f39018d53f681fa1c0ffe9570e6d73cde1b65d23bb557c846f442", size = 11681264 }, + { url = "https://files.pythonhosted.org/packages/27/5c/f5ae0a9564e04108c132e1139d60491c0abc621397fe79a50b3dc0bd704b/ruff-0.9.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:38840dbcef63948657fa7605ca363194d2fe8c26ce8f9ae12eee7f098c85ac8a", size = 11657554 }, + { url = "https://files.pythonhosted.org/packages/2a/83/c6926fa3ccb97cdb3c438bb56a490b395770c750bf59f9bc1fe57ae88264/ruff-0.9.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d56ba06da53536b575fbd2b56517f6f95774ff7be0f62c80b9e67430391eeb36", size = 11088959 }, + { url = "https://files.pythonhosted.org/packages/af/a7/42d1832b752fe969ffdbfcb1b4cb477cb271bed5835110fb0a16ef31ab81/ruff-0.9.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f7cb2a01da08244c50b20ccfaeb5972e4228c3c3a1989d3ece2bc4b1f996001", size = 11902041 }, + { url = "https://files.pythonhosted.org/packages/53/cf/1fffa09fb518d646f560ccfba59f91b23c731e461d6a4dedd21a393a1ff1/ruff-0.9.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:96d5c76358419bc63a671caac70c18732d4fd0341646ecd01641ddda5c39ca0b", size = 11421069 }, + { url = "https://files.pythonhosted.org/packages/09/27/bb8f1b7304e2a9431f631ae7eadc35550fe0cf620a2a6a0fc4aa3d736f94/ruff-0.9.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:deb8304636ed394211f3a6d46c0e7d9535b016f53adaa8340139859b2359a070", size = 12625095 }, + { url = "https://files.pythonhosted.org/packages/d7/ce/ab00bc9d3df35a5f1b64f5117458160a009f93ae5caf65894ebb63a1842d/ruff-0.9.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df455000bf59e62b3e8c7ba5ed88a4a2bc64896f900f311dc23ff2dc38156440", size = 13257797 }, + { url = "https://files.pythonhosted.org/packages/88/81/c639a082ae6d8392bc52256058ec60f493c6a4d06d5505bccface3767e61/ruff-0.9.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de92170dfa50c32a2b8206a647949590e752aca8100a0f6b8cefa02ae29dce80", size = 12763793 }, + { url = "https://files.pythonhosted.org/packages/b3/d0/0a3d8f56d1e49af466dc770eeec5c125977ba9479af92e484b5b0251ce9c/ruff-0.9.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d28532d73b1f3f627ba88e1456f50748b37f3a345d2be76e4c653bec6c3e393", size = 14386234 }, + { url = "https://files.pythonhosted.org/packages/04/70/e59c192a3ad476355e7f45fb3a87326f5219cc7c472e6b040c6c6595c8f0/ruff-0.9.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c746d7d1df64f31d90503ece5cc34d7007c06751a7a3bbeee10e5f2463d52d2", size = 12437505 }, + { url = "https://files.pythonhosted.org/packages/55/4e/3abba60a259d79c391713e7a6ccabf7e2c96e5e0a19100bc4204f1a43a51/ruff-0.9.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11417521d6f2d121fda376f0d2169fb529976c544d653d1d6044f4c5562516ee", size = 11884799 }, + { url = "https://files.pythonhosted.org/packages/a3/db/b0183a01a9f25b4efcae919c18fb41d32f985676c917008620ad692b9d5f/ruff-0.9.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b9d71c3879eb32de700f2f6fac3d46566f644a91d3130119a6378f9312a38e1", size = 11527411 }, + { url = "https://files.pythonhosted.org/packages/0a/e4/3ebfcebca3dff1559a74c6becff76e0b64689cea02b7aab15b8b32ea245d/ruff-0.9.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2e36c61145e70febcb78483903c43444c6b9d40f6d2f800b5552fec6e4a7bb9a", size = 12078868 }, + { url = "https://files.pythonhosted.org/packages/ec/b2/5ab808833e06c0a1b0d046a51c06ec5687b73c78b116e8d77687dc0cd515/ruff-0.9.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2f71d09aeba026c922aa7aa19a08d7bd27c867aedb2f74285a2639644c1c12f5", size = 12524374 }, + { url = "https://files.pythonhosted.org/packages/e0/51/1432afcc3b7aa6586c480142caae5323d59750925c3559688f2a9867343f/ruff-0.9.5-py3-none-win32.whl", hash = "sha256:134f958d52aa6fdec3b294b8ebe2320a950d10c041473c4316d2e7d7c2544723", size = 9853682 }, + { url = "https://files.pythonhosted.org/packages/b7/ad/c7a900591bd152bb47fc4882a27654ea55c7973e6d5d6396298ad3fd6638/ruff-0.9.5-py3-none-win_amd64.whl", hash = "sha256:78cc6067f6d80b6745b67498fb84e87d32c6fc34992b52bffefbdae3442967d6", size = 10865744 }, + { url = "https://files.pythonhosted.org/packages/75/d9/fde7610abd53c0c76b6af72fc679cb377b27c617ba704e25da834e0a0608/ruff-0.9.5-py3-none-win_arm64.whl", hash = "sha256:18a29f1a005bddb229e580795627d297dfa99f16b30c7039e73278cf6b5f9fa9", size = 10064595 }, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/7b/af302bebf22c749c56c9c3e8ae13190b5b5db37a33d9068652e8f73b7089/snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1", size = 86699 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/dc/c02e01294f7265e63a7315fe086dd1df7dacb9f840a804da846b96d01b96/snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a", size = 93002 }, +] + +[[package]] +name = "sphinx" +version = "8.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals-py" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/4b/95bdb36eaee30698f2d244d52e1b9e58642af56525d4b02fcd0f7312c27c/sphinx-8.2.1.tar.gz", hash = "sha256:e4b932951b9c18b039f73b72e4e63afe967d90408700ec222b981ac24647c01e", size = 8321376 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/aa/282768cff0039b227a923cb65686539bb606e448c594d4fdee4d2c7765a1/sphinx-8.2.1-py3-none-any.whl", hash = "sha256:b5d2bb3cdf6207fcacde9f92085d2b97667b05b9c346eaec426ca4be8af505e9", size = 3589415 }, +] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300 }, +] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530 }, +] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705 }, +] + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071 }, +] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743 }, +] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 }, +] + +[[package]] +name = "systemd-python" +version = "235" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/9e/ab4458e00367223bda2dd7ccf0849a72235ee3e29b36dce732685d9b7ad9/systemd-python-235.tar.gz", hash = "sha256:4e57f39797fd5d9e2d22b8806a252d7c0106c936039d1e71c8c6b8008e695c0a", size = 61677 } + +[[package]] +name = "tomlkit" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955 }, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, +] + +[[package]] +name = "urllib3" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, +] + +[[package]] +name = "virtualenv" +version = "20.29.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/88/dacc875dd54a8acadb4bcbfd4e3e86df8be75527116c91d8f9784f5e9cab/virtualenv-20.29.2.tar.gz", hash = "sha256:fdaabebf6d03b5ba83ae0a02cfe96f48a716f4fae556461d180825866f75b728", size = 4320272 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/fa/849483d56773ae29740ae70043ad88e068f98a6401aa819b5d6bee604683/virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a", size = 4301478 }, +]