Add additional package selector (#3196)

This commit is contained in:
Daniel Girtler 2025-02-24 17:57:26 +11:00 committed by GitHub
parent 4a477351e0
commit 74b41dea96
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 1305 additions and 470 deletions

View File

@ -56,16 +56,17 @@ def plugin(f, *args, **kwargs) -> None: # type: ignore[no-untyped-def]
plugins[f.__name__] = f plugins[f.__name__] = f
def _check_new_version() -> None: def _fetch_arch_db() -> None:
info("Checking version...") info("Fetching Arch Linux package database...")
try: try:
Pacman.run("-Sy") Pacman.run("-Sy")
except Exception as e: except Exception as e:
debug(f'Failed to perform version check: {e}') debug(f'Failed to sync Arch Linux package database: {e}')
info('Arch Linux mirrors are not reachable. Please check your internet connection')
exit(1) exit(1)
def _check_new_version() -> None:
info("Checking version...")
upgrade = None upgrade = None
try: try:
@ -85,8 +86,11 @@ def main() -> None:
OR straight as a module: python -m archinstall 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 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: if not arch_config_handler.args.offline:
_check_new_version() _fetch_arch_db()
if not arch_config_handler.args.skip_version_check:
_check_new_version()
script = arch_config_handler.args.script script = arch_config_handler.args.script

View File

@ -37,7 +37,7 @@ from .models.users import User
from .output import FormattedOutput from .output import FormattedOutput
from .profile.profile_menu import ProfileConfiguration from .profile.profile_menu import ProfileConfiguration
from .translationhandler import Language, translation_handler from .translationhandler import Language, translation_handler
from .utils.util import format_cols, get_password from .utils.util import get_password
if TYPE_CHECKING: if TYPE_CHECKING:
from collections.abc import Callable from collections.abc import Callable
@ -319,7 +319,8 @@ class GlobalMenu(AbstractMenu):
def _prev_additional_pkgs(self, item: MenuItem) -> str | None: def _prev_additional_pkgs(self, item: MenuItem) -> str | None:
if item.value: if item.value:
return format_cols(item.value, None) output = '\n'.join(sorted(item.value))
return output
return None return None
def _prev_additional_repos(self, item: MenuItem) -> str | None: def _prev_additional_repos(self, item: MenuItem) -> str | None:

View File

@ -3,13 +3,14 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING 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 ..locale import list_timezones
from ..models.audio_configuration import Audio, AudioConfiguration from ..models.audio_configuration import Audio, AudioConfiguration
from ..models.gen import AvailablePackage
from ..output import warn from ..output import warn
from ..packages.packages import validate_package_list
from ..translationhandler import Language from ..translationhandler import Language
if TYPE_CHECKING: if TYPE_CHECKING:
@ -163,40 +164,48 @@ def select_archinstall_language(languages: list[Language], preset: Language) ->
raise ValueError('Language selection not handled') 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) # 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(_('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(_('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: # there are over 15k packages so this needs to be quick
packages = value.split() if value else [] preset_packages = []
for p in preset:
if p in packages:
preset_packages.append(packages[p])
if len(packages) == 0: items = [
return None 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: result = SelectMenu(
return None group,
header=header,
# Verify packages that were given alignment=Alignment.LEFT,
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,
allow_reset=True, allow_reset=True,
edit_width=100, allow_skip=True,
validator=validator, multi=True,
default_text=' '.join(preset) preview_frame=FrameProperties.max('Package info'),
).input() preview_style=PreviewStyle.RIGHT,
preview_size='auto'
).run()
match result.type_: match result.type_:
case ResultType.Skip: case ResultType.Skip:
@ -204,8 +213,8 @@ def ask_additional_packages_to_install(preset: list[str] = []) -> list[str]:
case ResultType.Reset: case ResultType.Reset:
return [] return []
case ResultType.Selection: case ResultType.Selection:
packages = result.text() selected_pacakges: list[AvailablePackage] = result.get_values()
return packages.split(' ') return [pkg.name for pkg in selected_pacakges]
def add_number_of_parallel_downloads(preset: int | None = None) -> int | None: def add_number_of_parallel_downloads(preset: int | None = None) -> int | None:

View File

@ -1,6 +1,32 @@
from dataclasses import dataclass from dataclasses import dataclass
from enum import Enum
from functools import cached_property
from typing import Any, override 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 @dataclass
class PackageSearchResult: class PackageSearchResult:
@ -73,9 +99,9 @@ class PackageSearch:
) )
@dataclass class LocalPackage(BaseModel):
class LocalPackage:
name: str name: str
repository: str
version: str version: str
description: str description: str
architecture: str architecture: str
@ -97,16 +123,46 @@ class LocalPackage:
validated_by: str validated_by: str
provides: str provides: str
@property
def pkg_version(self) -> str:
return self.version
@override @override
def __eq__(self, other: object) -> bool: def __eq__(self, other: object) -> bool:
if not isinstance(other, LocalPackage): if not isinstance(other, LocalPackage):
return NotImplemented return NotImplemented
return self.pkg_version == other.pkg_version return self.version == other.version
def __lt__(self, other: 'LocalPackage') -> bool: 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

View File

@ -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

View File

@ -1,15 +1,19 @@
import dataclasses
import json import json
import ssl import ssl
from functools import lru_cache
from typing import TypeVar
from urllib.error import HTTPError from urllib.error import HTTPError
from urllib.parse import urlencode from urllib.parse import urlencode
from urllib.request import urlopen from urllib.request import urlopen
from urllib.response import addinfourl from urllib.response import addinfourl
from ..exceptions import PackageError, SysCallError 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 from ..pacman import Pacman
PackageType = TypeVar("PackageType", AvailablePackage, LocalPackage)
BASE_URL_PKG_SEARCH = 'https://archlinux.org/packages/search/json/' BASE_URL_PKG_SEARCH = 'https://archlinux.org/packages/search/json/'
# BASE_URL_PKG_CONTENT = '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/' 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) return list(valid_packages), list(invalid_packages)
def installed_package(package: str) -> LocalPackage: def installed_package(package: str) -> LocalPackage | None:
package_info = {} package_info = []
try: try:
for line in Pacman.run(f"-Q --info {package}"): package_info = Pacman.run(f'-Q --info {package}').decode().split('\n')
if b':' in line: return _parse_package_output(package_info, LocalPackage)
key, value = line.decode().split(':', 1)
package_info[key.strip().lower().replace(' ', '_')] = value.strip()
except SysCallError: except SysCallError:
pass pass
return LocalPackage( # pylint: disable=no-value-for-parameter return None
{field.name: package_info.get(field.name) for field in dataclasses.fields(LocalPackage)} # type: ignore
)
@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)

View File

@ -2,20 +2,18 @@ from __future__ import annotations
import curses import curses
import curses.panel import curses.panel
import dataclasses
import os import os
import signal import signal
import sys import sys
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from collections.abc import Callable from collections.abc import Callable
from curses.textpad import Textbox from curses.textpad import Textbox
from dataclasses import dataclass
from types import FrameType, TracebackType from types import FrameType, TracebackType
from typing import TYPE_CHECKING, Literal, override from typing import TYPE_CHECKING, Literal, override
from ..lib.output import debug from ..lib.output import debug
from .help import Help from .help import Help
from .menu_item import MenuItem, MenuItemGroup from .menu_item import MenuItem, MenuItemGroup, MenuItemsState
from .types import ( from .types import (
SCROLL_INTERVAL, SCROLL_INTERVAL,
STYLE, STYLE,
@ -23,7 +21,6 @@ from .types import (
Chars, Chars,
FrameProperties, FrameProperties,
FrameStyle, FrameStyle,
MenuCell,
MenuKeys, MenuKeys,
Orientation, Orientation,
PreviewStyle, PreviewStyle,
@ -121,7 +118,6 @@ class AbstractCurses(metaclass=ABCMeta):
return full_header return full_header
@dataclass
class AbstractViewport: class AbstractViewport:
def __init__(self) -> None: def __init__(self) -> None:
pass pass
@ -397,24 +393,6 @@ class EditViewport(AbstractViewport):
self._textbox.edit(self.process_key) # type: ignore[arg-type] 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): class Viewport(AbstractViewport):
def __init__( def __init__(
self, self,
@ -440,8 +418,6 @@ class Viewport(AbstractViewport):
self._main_win.nodelay(False) self._main_win.nodelay(False)
self._main_win.standout() self._main_win.standout()
self._state: ViewportState | None = None
def getch(self) -> int: def getch(self) -> int:
return self._main_win.getch() return self._main_win.getch()
@ -451,12 +427,13 @@ class Viewport(AbstractViewport):
def update( def update(
self, self,
lines: list[ViewportEntry], entries: list[ViewportEntry],
cur_pos: int = 0, cur_pos: int = 0,
scroll_pos: int | None = None scroll_pos: int | None = None
) -> None: ) -> None:
self._state = self._get_viewport_state(lines, cur_pos, scroll_pos) # self._state = self._get_viewport_state(lines, cur_pos, scroll_pos)
visible_entries = self._adjust_entries_row(self._state.displayed_entries) # visible_entries = self._adjust_entries_row(self._state.displayed_entries)
visible_entries = entries
if self._frame: if self._frame:
visible_entries = self.add_frame( visible_entries = self.add_frame(
@ -464,7 +441,7 @@ class Viewport(AbstractViewport):
self.width, self.width,
self.height, self.height,
frame=self._frame, frame=self._frame,
scroll_pct=self._state.scroll_pct scroll_pct=scroll_pos
) )
x_offset = 0 x_offset = 0
@ -484,121 +461,6 @@ class Viewport(AbstractViewport):
self._main_win.refresh() 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): class EditMenu(AbstractCurses):
def __init__( def __init__(
@ -850,6 +712,7 @@ class SelectMenu(AbstractCurses):
self._header = header self._header = header
header_offset = self._get_header_offset(header) header_offset = self._get_header_offset(header)
self._headers = self.get_header_entries(header, offset=header_offset) self._headers = self.get_header_entries(header, offset=header_offset)
if self._interrupt_warning is None: if self._interrupt_warning is None:
@ -860,9 +723,7 @@ class SelectMenu(AbstractCurses):
else: else:
self._horizontal_cols = 1 self._horizontal_cols = 1
self._row_entries: list[list[MenuCell]] = []
self._prev_scroll_pos: int = 0 self._prev_scroll_pos: int = 0
self._cur_pos: int | None = None
self._visible_entries: list[ViewportEntry] = [] self._visible_entries: list[ViewportEntry] = []
self._max_height, self._max_width = Tui.t().max_yx self._max_height, self._max_width = Tui.t().max_yx
@ -875,6 +736,14 @@ class SelectMenu(AbstractCurses):
self._init_viewports(preview_size) 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: def _get_header_offset(self, header: str | None) -> int:
# WARNING: any changes here will impact the list manager table view # WARNING: any changes here will impact the list manager table view
if self._orientation == Orientation.HORIZONTAL: if self._orientation == Orientation.HORIZONTAL:
@ -883,7 +752,7 @@ class SelectMenu(AbstractCurses):
lines = header.split('\n') if header else [] lines = header.split('\n') if header else []
table_header = [line for line in lines if '|' in line] table_header = [line for line in lines if '|' in line]
longest_header = len(table_header[0]) if table_header else 0 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) delta = abs(longest_header - longest_entry)
offset = delta + 3 # 3 because it seems to align it... offset = delta + 3 # 3 because it seems to align it...
@ -1050,7 +919,7 @@ class SelectMenu(AbstractCurses):
if preview_size == 'auto': if preview_size == 'auto':
match self._preview_style: match self._preview_style:
case PreviewStyle.RIGHT: case PreviewStyle.RIGHT:
menu_width = self._item_group.max_width + 5 menu_width = self._item_group.get_max_width() + 5
if self._multi: if self._multi:
menu_width += 5 menu_width += 5
prev_size = self._max_width - menu_width prev_size = self._max_width - menu_width
@ -1074,8 +943,8 @@ class SelectMenu(AbstractCurses):
def _draw(self) -> None: def _draw(self) -> None:
footer_entries = self._footer_entries() footer_entries = self._footer_entries()
vp_entries = self._get_row_entries() items = self._items_state.get_view_items()
self._cur_pos = self._get_cursor_pos() vp_entries = self._item_to_vp_entry(items)
if self._help_vp: if self._help_vp:
self._update_viewport(self._help_vp, [self.help_entry()]) self._update_viewport(self._help_vp, [self.help_entry()])
@ -1084,11 +953,7 @@ class SelectMenu(AbstractCurses):
self._update_viewport(self._header_vp, self._headers) self._update_viewport(self._header_vp, self._headers)
if self._menu_vp: if self._menu_vp:
self._update_viewport( self._update_viewport(self._menu_vp, vp_entries)
self._menu_vp,
vp_entries,
cur_pos=self._cur_pos
)
if vp_entries: if vp_entries:
self._update_preview() self._update_preview()
@ -1109,21 +974,8 @@ class SelectMenu(AbstractCurses):
else: else:
viewport.update([]) viewport.update([])
def _get_cursor_pos(self) -> int: def _get_col_widths(self, items: list[list[MenuItem]]) -> list[int]:
for idx, cells in enumerate(self._row_entries): cols_widths = self._calc_col_widths(items, self._horizontal_cols)
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)
return [col_width + len(self._cursor_char) + self._item_distance() for col_width in cols_widths] return [col_width + len(self._cursor_char) + self._item_distance() for col_width in cols_widths]
def _item_distance(self) -> int: def _item_distance(self) -> int:
@ -1132,68 +984,56 @@ class SelectMenu(AbstractCurses):
else: else:
return self._column_spacing return self._column_spacing
def _get_row_entries(self) -> list[ViewportEntry]: def _item_to_vp_entry(self, items: list[list[MenuItem]]) -> list[ViewportEntry]:
cells = self._assemble_menu_cells()
entries = [] 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)] for row_idx, row in enumerate(items):
cols_widths = self._get_col_widths()
for row_idx, row in enumerate(self._row_entries):
cur_pos = len(self._cursor_char) cur_pos = len(self._cursor_char)
for col_idx, cell in enumerate(row): for col_idx, cell in enumerate(row):
cur_text = '' cur_text = ''
style = STYLE.NORMAL style = STYLE.NORMAL
if cell.item == self._item_group.focus_item: if cell == self._item_group.focus_item:
cur_text = self._cursor_char cur_text = self._cursor_char
style = STYLE.MENU_STYLE style = STYLE.MENU_STYLE
entries += [ViewportEntry(cur_text, row_idx, cur_pos - len(self._cursor_char), STYLE.CURSOR_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)] menu_item_text = self._menu_item_text(cell)
cur_pos += len(cell.text) entries += [ViewportEntry(menu_item_text, row_idx, cur_pos, style)]
cur_pos += len(menu_item_text)
if col_idx < len(row) - 1: 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)] entries += [ViewportEntry(' ' * spacer_len, row_idx, cur_pos, STYLE.NORMAL)]
cur_pos += spacer_len cur_pos += spacer_len
return entries return entries
def _calc_col_widths( def _calc_col_widths(self, rows: list[list[MenuItem]], columns: int) -> list[int]:
self,
row_chunks: list[list[MenuCell]],
cols: int
) -> list[int]:
col_widths = [] col_widths = []
for col in range(cols):
for row in rows:
col_entries = [] col_entries = []
for row in row_chunks: for column in range(columns):
if col < len(row): if column < len(row):
col_entries += [len(row[col].text)] col_entries += [len(row[column].text)]
if col_entries: if col_entries:
col_widths += [max(col_entries) if col_entries else 0] col_widths += [max(col_entries)]
return col_widths return col_widths
def _assemble_menu_cells(self) -> list[MenuCell]: def _menu_item_text(self, item: MenuItem) -> str:
items = self._get_visible_items() item_text = ''
entries = []
for item in items: if self._multi and not item.is_empty():
item_text = '' item_text += self._multi_prefix(item)
if self._multi and not item.is_empty(): item_text += self._item_group.get_item_text(item)
item_text += self._multi_prefix(item) return item_text
item_text += self._item_group.get_item_text(item)
entries += [MenuCell(item, item_text)]
return entries
def _update_preview(self) -> None: def _update_preview(self) -> None:
if not self._preview_vp: if not self._preview_vp:
@ -1214,15 +1054,64 @@ class SelectMenu(AbstractCurses):
preview_text = action_text.split('\n') preview_text = action_text.split('\n')
entries = [ViewportEntry(e, idx, 0, STYLE.NORMAL) for idx, e in enumerate(preview_text)] 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: self._preview_vp.update(prev_entries, scroll_pos=scroll_pct)
total_rows = max([e.row for e in entries]) + 1 # rows start with 0 and we need the count
if self._prev_scroll_pos >= total_rows: def _get_scroll_pct(
self._prev_scroll_pos = total_rows - 2 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: elif self._prev_scroll_pos < 0:
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 Result(ResultType.Selection, self._item_group.focus_item)
return None return None
case MenuKeys.MENU_UP | MenuKeys.MENU_DOWN | MenuKeys.MENU_LEFT | MenuKeys.MENU_RIGHT: case MenuKeys.MENU_DOWN | MenuKeys.MENU_RIGHT:
self._focus_item(handle) self._focus_item('next')
case MenuKeys.MENU_UP | MenuKeys.MENU_LEFT:
self._focus_item('prev')
case MenuKeys.MENU_START: case MenuKeys.MENU_START:
self._item_group.focus_first() self._focus_item('first')
case MenuKeys.MENU_END: case MenuKeys.MENU_END:
self._item_group.focus_last() self._focus_item('last')
case MenuKeys.MULTI_SELECT: case MenuKeys.MULTI_SELECT:
if self._multi: if self._multi:
self._item_group.select_current_item() self._item_group.select_current_item()
@ -1315,7 +1206,7 @@ class SelectMenu(AbstractCurses):
if self._allow_skip: if self._allow_skip:
return Result(ResultType.Skip, None) return Result(ResultType.Skip, None)
case MenuKeys.NUM_KEYS: case MenuKeys.NUM_KEYS:
self._item_group.set_focus_item_index(key - 49) self._item_group.focus_index(key - 49)
case MenuKeys.SCROLL_DOWN: case MenuKeys.SCROLL_DOWN:
self._prev_scroll_pos += SCROLL_INTERVAL self._prev_scroll_pos += SCROLL_INTERVAL
case MenuKeys.SCROLL_UP: case MenuKeys.SCROLL_UP:
@ -1323,56 +1214,22 @@ class SelectMenu(AbstractCurses):
case _: case _:
pass pass
self._draw()
return None return None
def _focus_item(self, key: MenuKeys) -> None: def _focus_item(self, direction: Literal['next' | 'prev' | 'first' | 'last']) -> None:
focus_item = self._item_group.focus_item # reset the preview scroll as the newly focused item
next_row = 0 # may have a different preview row count and it'll blow up
next_col = 0 self._prev_scroll_pos = 0
for row_idx, row in enumerate(self._row_entries): match direction:
for col_idx, cell in enumerate(row): case 'next':
if cell.item == focus_item: self._item_group.focus_next()
match key: case 'prev':
case MenuKeys.MENU_UP: self._item_group.focus_prev()
next_row = row_idx - 1 case 'first':
next_col = col_idx self._item_group.focus_first()
case 'last':
if next_row < 0: self._item_group.focus_last()
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)
class Tui: class Tui:
@ -1447,10 +1304,10 @@ class Tui:
endl: str | None = '\n', endl: str | None = '\n',
clear_screen: bool = False clear_screen: bool = False
) -> None: ) -> None:
if Tui._t is None: if clear_screen:
if clear_screen: os.system('clear')
os.system('clear')
if Tui._t is None:
print(text, end=endl) print(text, end=endl)
sys.stdout.flush() sys.stdout.flush()

View File

@ -1,5 +1,12 @@
from collections.abc import Callable
from dataclasses import dataclass, field from dataclasses import dataclass, field
from enum import Enum from enum import Enum
from typing import TYPE_CHECKING
from archinstall.lib.translationhandler import DeferredTranslation
if TYPE_CHECKING:
_: Callable[[str], DeferredTranslation]
class HelpTextGroupId(Enum): class HelpTextGroupId(Enum):
@ -28,52 +35,70 @@ class HelpGroup:
class Help: class Help:
general = HelpGroup( # the groups needs to be classmethods not static methods
group_id=HelpTextGroupId.GENERAL, # because they rely on the DeferredTranslation setup first;
group_entries=[ # if they are static methods, they will be called before the
HelpText('Show help', ['Ctrl+h']), # translation setup is done
HelpText('Exit help', ['Esc']),
]
)
navigation = HelpGroup( @staticmethod
group_id=HelpTextGroupId.NAVIGATION, def general() -> HelpGroup:
group_entries=[ return HelpGroup(
HelpText('Preview scroll up', ['PgUp']), group_id=HelpTextGroupId.GENERAL,
HelpText('Preview scroll down', ['PgDown']), group_entries=[
HelpText('Move up', ['k', '']), HelpText(str(_('Show help')), ['Ctrl+h']),
HelpText('Move down', ['j', '']), HelpText(str(_('Exit help')), ['Esc']),
HelpText('Move right', ['l', '']), ]
HelpText('Move left', ['h', '']), )
HelpText('Jump to entry', ['1..9'])
]
)
selection = HelpGroup( @staticmethod
group_id=HelpTextGroupId.SELECTION, def navigation() -> HelpGroup:
group_entries=[ return HelpGroup(
HelpText('Skip selection (if available)', ['Esc']), group_id=HelpTextGroupId.NAVIGATION,
HelpText('Reset selection (if available)', ['Ctrl+c']), group_entries=[
HelpText('Select on single select', ['Enter']), HelpText(str(_('Preview scroll up')), ['PgUp']),
HelpText('Select on select', ['Space', 'Tab']), HelpText(str(_('Preview scroll down')), ['PgDown']),
HelpText('Reset', ['Ctrl-C']), HelpText(str(_('Move up')), ['k', '']),
HelpText('Skip selection menu', ['Esc']), HelpText(str(_('Move down')), ['j', '']),
] HelpText(str(_('Move right')), ['l', '']),
) HelpText(str(_('Move left')), ['h', '']),
HelpText(str(_('Jump to entry')), ['1..9'])
]
)
search = HelpGroup( @staticmethod
group_id=HelpTextGroupId.SEARCH, def selection() -> HelpGroup:
group_entries=[ return HelpGroup(
HelpText('Start search mode', ['/']), group_id=HelpTextGroupId.SELECTION,
HelpText('Exit search mode', ['Esc']), 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 @staticmethod
def get_help_text() -> str: def get_help_text() -> str:
help_output = '' help_output = ''
help_texts = [Help.general, Help.navigation, Help.selection, Help.search] help_texts = [
max_desc_width = max([help.get_desc_width() for help in 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]) max_key_width = max([help.get_key_width() for help in help_texts])
for help_group in help_texts: for help_group in help_texts:

View File

@ -2,6 +2,7 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from dataclasses import dataclass, field from dataclasses import dataclass, field
from functools import cached_property
from typing import TYPE_CHECKING, Any, ClassVar from typing import TYPE_CHECKING, Any, ClassVar
from ..lib.output import unicode_ljust from ..lib.output import unicode_ljust
@ -66,33 +67,36 @@ class MenuItem:
return None return None
@dataclass
class MenuItemGroup: class MenuItemGroup:
menu_items: list[MenuItem] def __init__(
focus_item: MenuItem | None = None self,
default_item: MenuItem | None = None menu_items: list[MenuItem],
selected_items: list[MenuItem] = field(default_factory=list) focus_item: MenuItem | None = None,
sort_items: bool = False default_item: MenuItem | None = None,
checkmarks: bool = False sort_items: bool = False,
checkmarks: bool = False
_filter_pattern: str = '' ) -> None:
if len(menu_items) < 1:
def __post_init__(self) -> None:
if len(self.menu_items) < 1:
raise ValueError('Menu must have at least one item') raise ValueError('Menu must have at least one item')
if self.sort_items: if sort_items:
self.menu_items = sorted(self.menu_items, key=lambda x: x.text) menu_items = sorted(menu_items, key=lambda x: x.text)
if not self.focus_item: if not focus_item:
if self.selected_items: focus_item = menu_items[0]
self.focus_item = self.selected_items[0]
else:
self.focus_item = self.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') 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: def find_by_key(self, key: str) -> MenuItem:
for item in self.menu_items: for item in self.menu_items:
if item.key == key: if item.key == key:
@ -100,6 +104,9 @@ class MenuItemGroup:
raise ValueError(f'No key found for: {key}') 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 @staticmethod
def yes_no() -> 'MenuItemGroup': def yes_no() -> 'MenuItemGroup':
return MenuItemGroup( return MenuItemGroup(
@ -137,39 +144,30 @@ class MenuItemGroup:
if values: if values:
self.set_focus_by_value(values[0]) self.set_focus_by_value(values[0])
def index_of(self, item: MenuItem) -> int: def index_focus(self) -> int | None:
return self.items.index(item) if self.focus_item and self.items:
return self.items.index(self.focus_item)
def index_focus(self) -> int: return None
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])
@property @property
def size(self) -> int: def size(self) -> int:
return len(self.items) return len(self.items)
@property def get_max_width(self) -> int:
def max_width(self) -> int:
# use the menu_items not the items here otherwise the preview # use the menu_items not the items here otherwise the preview
# will get resized all the time when a filter is applied # 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]) 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]) return max([len(item.text) for item in self.menu_items])
def get_item_text(self, item: MenuItem) -> str: def get_item_text(self, item: MenuItem) -> str:
if item.is_empty(): if item.is_empty():
return '' return ''
max_width = self._max_item_width() max_width = self._max_items_text_width
display_text = item.get_display_value() display_text = item.get_display_value()
default_text = self._default_suffix(item) default_text = self._default_suffix(item)
@ -178,7 +176,7 @@ class MenuItemGroup:
if display_text: if display_text:
text = f'{text}{spacing}{display_text}' text = f'{text}{spacing}{display_text}'
elif self.checkmarks: elif self._checkmarks:
from .types import Chars from .types import Chars
if item.has_value(): if item.has_value():
@ -197,42 +195,38 @@ class MenuItemGroup:
return str(_(' (default)')) return str(_(' (default)'))
return '' return ''
@property @cached_property
def items(self) -> list[MenuItem]: def items(self) -> list[MenuItem]:
f = self._filter_pattern.lower() _filter = self._filter_pattern.lower()
items = filter(lambda item: item.is_empty() or f in item.text.lower(), self.menu_items) items = filter(lambda item: item.is_empty() or _filter in item.text.lower(), self.menu_items)
return list(items) return list(items)
@property @property
def filter_pattern(self) -> str: def filter_pattern(self) -> str:
return self._filter_pattern return self._filter_pattern
def has_filter(self) -> bool:
return self._filter_pattern != ''
def set_filter_pattern(self, pattern: str) -> None: def set_filter_pattern(self, pattern: str) -> None:
self._filter_pattern = pattern 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: def append_filter(self, pattern: str) -> None:
self._filter_pattern += pattern self._filter_pattern += pattern
self.reload_focus_itme() delattr(self, 'items') # resetting the cache
self._reload_focus_item()
def reduce_filter(self) -> None: def reduce_filter(self) -> None:
self._filter_pattern = self._filter_pattern[:-1] 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: def _reload_focus_item(self) -> None:
items = self.items if len(self.items) > 0:
non_empty_items = [item for item in items if not item.is_empty()] if self.focus_item not in self.items:
if index < 0 or index >= len(non_empty_items): self.focus_first()
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 is_item_selected(self, item: MenuItem) -> bool: def is_item_selected(self, item: MenuItem) -> bool:
return item in self.selected_items return item in self.selected_items
@ -244,67 +238,71 @@ class MenuItemGroup:
else: else:
self.selected_items.append(self.focus_item) self.selected_items.append(self.focus_item)
def is_focused(self, item: MenuItem) -> bool: def focus_index(self, index: int) -> None:
if isinstance(self.focus_item, list): enabled = self.get_enabled_items()
return item in self.focus_item self.focus_item = enabled[index]
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_first(self) -> None: def focus_first(self) -> None:
first_item = self.get_first_item() first_item: MenuItem | None = self.items[0]
if first_item:
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 self.focus_item = first_item
def focus_last(self) -> None: def focus_last(self) -> None:
last_item = self.get_last_item() last_item: MenuItem | None = self.items[-1]
if last_item:
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 self.focus_item = last_item
def focus_prev(self, skip_empty: bool = True) -> None: 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: if item is not None:
return self.focus_item = item
if self.focus_item == items[0]: def focus_next(self, skip_not_enabled: bool = True) -> None:
self.focus_item = items[-1] assert self.focus_item is not None
else: item = self._find_next_selectable_item(self.items, self.focus_item, 1)
self.focus_item = items[items.index(self.focus_item) - 1]
if self.focus_item.is_empty() and skip_empty: if item is not None:
self.focus_prev(skip_empty) self.focus_item = item
def focus_next(self, skip_empty: bool = True) -> None: def get_focus_index(self) -> int:
items = self.items return self.items.index(self.focus_item)
if self.focus_item not in items: def _find_next_selectable_item(
return self,
items: list[MenuItem],
start_item: MenuItem,
direction: int
) -> MenuItem | None:
index = self.items.index(start_item)
if self.focus_item == items[-1]: start = index + direction
self.focus_item = items[0] end = 0
else:
self.focus_item = items[items.index(self.focus_item) + 1]
if self.focus_item.is_empty() and skip_empty: if direction == 1:
self.focus_next(skip_empty) 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: def is_mandatory_fulfilled(self) -> bool:
for item in self.menu_items: for item in self.menu_items:
@ -318,14 +316,20 @@ class MenuItemGroup:
return max(spaces) return max(spaces)
return 0 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: if not item.enabled:
return False return False
for dep in item.dependencies: for dep in item.dependencies:
if isinstance(dep, str): if isinstance(dep, str):
item = self.find_by_key(dep) 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 return False
else: else:
return dep() return dep()
@ -336,3 +340,103 @@ class MenuItemGroup:
return False return False
return True 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()

739
uv.lock Normal file
View File

@ -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 },
]